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/.changeset/short-chefs-stare.md b/.changeset/short-chefs-stare.md new file mode 100644 index 000000000000..32bf84b5b390 --- /dev/null +++ b/.changeset/short-chefs-stare.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +feat: use the Vite Environment API in development, preview, build analysis and prerendering diff --git a/.changeset/tidy-toes-sort.md b/.changeset/tidy-toes-sort.md new file mode 100644 index 000000000000..d30a43e3e07a --- /dev/null +++ b/.changeset/tidy-toes-sort.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: move `adapter` option from `svelte.config.js` to the SvelteKit Vite plugin in `vite.config.js` diff --git a/documentation/docs/25-build-and-deploy/20-adapters.md b/documentation/docs/25-build-and-deploy/20-adapters.md index 0e6062a5101a..489a9fb19c08 100644 --- a/documentation/docs/25-build-and-deploy/20-adapters.md +++ b/documentation/docs/25-build-and-deploy/20-adapters.md @@ -16,32 +16,34 @@ Additional [community-provided adapters](/packages#sveltekit-adapters) exist for ## Using adapters -Your adapter is specified in `svelte.config.js`: +Your adapter is specified in `vite.config.js`: ```js -/// file: svelte.config.js +/// file: vite.config.js // @filename: ambient.d.ts declare module 'svelte-adapter-foo' { - const adapter: (opts: any) => import('@sveltejs/kit').Adapter; + const adapter: (opts?: any) => import('@sveltejs/kit').Adapter; export default adapter; } // @filename: index.js // ---cut--- -import adapter from 'svelte-adapter-foo'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // adapter options go here +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; ++++import adapter from 'svelte-adapter-foo';+++ + +export default defineConfig({ + plugins: [ + sveltekit({ + +++adapter: adapter()+++ }) - } -}; - -export default config; + ] +}); ``` +> [!LEGACY] +> The `adapter` option was moved to the SvelteKit Vite plugin in SvelteKit 3.0.0. In earlier versions, you had to add it to the `kit` property in the `svelte.config.js` file instead. + ## Platform-specific context Some adapters may have access to additional information about the request. For example, Cloudflare Workers can access an `env` object containing KV namespaces etc. This can be passed to the `RequestEvent` used in [hooks](hooks) and [server routes](routing#server) as the `platform` property — consult each adapter's documentation to learn more. diff --git a/documentation/docs/25-build-and-deploy/40-adapter-node.md b/documentation/docs/25-build-and-deploy/40-adapter-node.md index 70f3ce553af5..49baf196ad41 100644 --- a/documentation/docs/25-build-and-deploy/40-adapter-node.md +++ b/documentation/docs/25-build-and-deploy/40-adapter-node.md @@ -6,23 +6,23 @@ To generate a standalone Node server, use [`adapter-node`](https://github.com/sv ## Usage -Install with `npm i -D @sveltejs/adapter-node`, then add the adapter to your `svelte.config.js`: +Install with `npm i -D @sveltejs/adapter-node`, then add the adapter to your `vite.config.js`: ```js // @errors: 2307 -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-node'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; +export default defineConfig({ + plugins: [sveltekit({ adapter: adapter() })] +}); ``` +> [!LEGACY] +> The `adapter` option was moved to the SvelteKit Vite plugin in SvelteKit 3.0.0. In earlier versions, you had to add it to the `kit` property in the `svelte.config.js` file instead. + ## Deploying First, build your app with `npm run build`. This will create the production server in the output directory specified in the adapter options, defaulting to `build`. @@ -150,22 +150,23 @@ The adapter can be configured with various options: ```js // @errors: 2307 -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-node'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // default options are shown - out: 'build', - precompress: true, - envPrefix: '' - }) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // default options are shown + out: 'build', + precompress: true, + envPrefix: '' + }) + }) + ] +}); ``` ### out diff --git a/documentation/docs/25-build-and-deploy/50-adapter-static.md b/documentation/docs/25-build-and-deploy/50-adapter-static.md index 52263273e59d..0e4daa566d6e 100644 --- a/documentation/docs/25-build-and-deploy/50-adapter-static.md +++ b/documentation/docs/25-build-and-deploy/50-adapter-static.md @@ -8,28 +8,29 @@ This will prerender your entire site as a collection of static files. If you'd l ## Usage -Install with `npm i -D @sveltejs/adapter-static`, then add the adapter to your `svelte.config.js`: +Install with `npm i -D @sveltejs/adapter-static`, then add the adapter to your `vite.config.js`: ```js -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-static'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // default options are shown. On some platforms - // these options are set automatically — see below - pages: 'build', - assets: 'build', - fallback: undefined, - precompress: false, - strict: true - }) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // default options are shown. On some platforms + // these options are set automatically — see below + pages: 'build', + assets: 'build', + fallback: undefined, + precompress: false, + strict: true + }) + }) + ] +}); ``` ...and add the [`prerender`](page-options#prerender) option to your root layout: @@ -46,6 +47,9 @@ export const prerender = true; > [!NOTE] You must ensure SvelteKit's [`ssr`](page-options#ssr) option isn't set to `false`. Otherwise, prerendering will save an empty 'shell' page instead of the fully rendered content. +> [!LEGACY] +> The `adapter` option was moved to the SvelteKit Vite plugin in SvelteKit 3.0.0. In earlier versions, you had to add it to the `kit` property in the `svelte.config.js` file instead. + ## Zero-config support Some platforms have zero-config support (more to come in future): @@ -55,17 +59,18 @@ Some platforms have zero-config support (more to come in future): On these platforms, you should omit the adapter options so that `adapter-static` can provide the optimal configuration: ```js -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-static'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter(---{...}---) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter(---{...}---) + }) + ] +}); ``` ## Options @@ -103,14 +108,9 @@ A config for GitHub Pages might look like the following: ```js // @errors: 2322 /// file: svelte.config.js -import adapter from '@sveltejs/adapter-static'; - /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter({ - fallback: '404.html' - }), paths: { base: process.argv.includes('dev') ? '' : process.env.BASE_PATH } @@ -120,6 +120,23 @@ const config = { export default config; ``` +```js +/// file: vite.config.js +import adapter from '@sveltejs/adapter-static'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + fallback: '404.html' + }) + }) + ] +}); +``` + You can use GitHub actions to automatically deploy your site to GitHub Pages when you make a change. Here's an example workflow: ```yaml diff --git a/documentation/docs/25-build-and-deploy/55-single-page-apps.md b/documentation/docs/25-build-and-deploy/55-single-page-apps.md index 2826c28f8d2c..b275c8fb52f8 100644 --- a/documentation/docs/25-build-and-deploy/55-single-page-apps.md +++ b/documentation/docs/25-build-and-deploy/55-single-page-apps.md @@ -16,22 +16,23 @@ First, disable SSR for the pages you don't want to prerender. These pages will b export const ssr = false; ``` -If you don't have any server-side logic (i.e. `+page.server.js`, `+layout.server.js` or `+server.js` files) you can use [`adapter-static`](adapter-static) to create your SPA. Install `adapter-static` with `npm i -D @sveltejs/adapter-static` and add it to your `svelte.config.js` with the `fallback` option: +If you don't have any server-side logic (i.e. `+page.server.js`, `+layout.server.js` or `+server.js` files) you can use [`adapter-static`](adapter-static) to create your SPA. Install `adapter-static` with `npm i -D @sveltejs/adapter-static` and add it to your `vite.config.js` with the `fallback` option: ```js -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-static'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - fallback: '200.html' // may differ from host to host - }) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + fallback: '200.html' // may differ from host to host + }) + }) + ] +}); ``` The `fallback` page is an HTML page created by SvelteKit from your page template (e.g. `app.html`) that loads your app and navigates to the correct route. For example [Surge](https://surge.sh/help/adding-a-200-page-for-client-side-routing), a static web host, lets you add a `200.html` file that will handle any requests that don't correspond to static assets or prerendered pages. @@ -40,6 +41,9 @@ On some hosts it may be something else entirely — consult your platform's docu > [!NOTE] Note that the fallback page will always contain absolute asset paths (i.e. beginning with `/` rather than `.`) regardless of the value of [`paths.relative`](configuration#paths), since it is used to respond to requests for arbitrary paths. +> [!LEGACY] +> The `adapter` option was moved to the SvelteKit Vite plugin in SvelteKit 3.0.0. In earlier versions, you had to add it to the `kit` property in the `svelte.config.js` file instead. + ## Prerendering individual pages If you want certain pages to be prerendered, you can re-enable `ssr` alongside `prerender` for just those parts of your app: diff --git a/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md b/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md index ccd2a26ca2fc..a426548f00f2 100644 --- a/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md +++ b/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md @@ -14,35 +14,40 @@ This adapter will be installed by default when you use [`adapter-auto`](adapter- ## Usage -Install with `npm i -D @sveltejs/adapter-cloudflare`, then add the adapter to your `svelte.config.js`: +Install with `npm i -D @sveltejs/adapter-cloudflare`, then add the adapter to your `vite.config.js`: ```js -/// file: svelte.config.js +// @errors: 2307 +/// file: vite.config.js import adapter from '@sveltejs/adapter-cloudflare'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // See below for an explanation of these options - config: undefined, - platformProxy: { - configPath: undefined, - environment: undefined, - persist: undefined - }, - fallback: 'plaintext', - routes: { - include: ['/*'], - exclude: [''] - } - }) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // See below for an explanation of these options + config: undefined, + platformProxy: { + configPath: undefined, + environment: undefined, + persist: undefined + }, + fallback: 'plaintext', + routes: { + include: ['/*'], + exclude: [''] + } + }) + }) + ] +}); ``` +> [!LEGACY] +> The `adapter` option was moved to the SvelteKit Vite plugin in SvelteKit 3.0.0. In earlier versions, you had to add it to the `kit` property in the `svelte.config.js` file instead. + ## Options ### config @@ -222,18 +227,33 @@ Cloudflare no longer recommends using [Workers Sites](https://developers.cloudfl // @errors: 2307 /// file: svelte.config.js ---import adapter from '@sveltejs/adapter-cloudflare-workers';--- -+++import adapter from '@sveltejs/adapter-cloudflare';+++ /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter() + ---adapter: adapter()--- } }; export default config; ``` +```js +// @errors: 2307 +/// file: vite.config.js ++++import adapter from '@sveltejs/adapter-cloudflare';+++ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + +++adapter: adapter()+++ + }) + ] +}); +``` + ### wrangler.toml ```toml diff --git a/documentation/docs/25-build-and-deploy/80-adapter-netlify.md b/documentation/docs/25-build-and-deploy/80-adapter-netlify.md index 4e44a34ef100..4a1639a34d5e 100644 --- a/documentation/docs/25-build-and-deploy/80-adapter-netlify.md +++ b/documentation/docs/25-build-and-deploy/80-adapter-netlify.md @@ -8,30 +8,30 @@ This adapter will be installed by default when you use [`adapter-auto`](adapter- ## Usage -Install with `npm i -D @sveltejs/adapter-netlify`, then add the adapter to your `svelte.config.js`: +Install with `npm i -D @sveltejs/adapter-netlify`, then add the adapter to your `vite.config.js`: ```js -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-netlify'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - // default options are shown - adapter: adapter({ - // if true, will create a Netlify Edge Function rather - // than using standard Node-based functions - edge: false, - - // if true, will split your app into multiple functions - // instead of creating a single one for the entire app. - // if `edge` is true, this option cannot be used - split: false - }) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // if true, will create a Netlify Edge Function rather + // than using standard Node-based functions + edge: false, + + // if true, will split your app into multiple functions + // instead of creating a single one for the entire app. + // if `edge` is true, this option cannot be used + split: false + }) + }) + ] +}); ``` Then, make sure you have a [netlify.toml](https://docs.netlify.com/configure-builds/file-based-configuration) file in the project root. This will determine where to write static assets based on the `build.publish` settings, as per this sample configuration: @@ -44,6 +44,9 @@ Then, make sure you have a [netlify.toml](https://docs.netlify.com/configure-bui If the `netlify.toml` file or the `build.publish` value is missing, a default value of `"build"` will be used. Note that if you have set the publish directory in the Netlify UI to something else then you will need to set it in `netlify.toml` too, or use the default value of `"build"`. +> [!LEGACY] +> The `adapter` option was moved to the SvelteKit Vite plugin in SvelteKit 3.0.0. In earlier versions, you had to add it to the `kit` property in the `svelte.config.js` file instead. + ### Node version New projects will use the current Node LTS version by default. However, if you're upgrading a project you created a while ago it may be stuck on an older version. See [the Netlify docs](https://docs.netlify.com/configure-builds/manage-dependencies/#node-js-and-javascript) for details on manually specifying a current Node version. @@ -53,21 +56,23 @@ New projects will use the current Node LTS version by default. However, if you'r SvelteKit supports [Netlify Edge Functions](https://docs.netlify.com/build/edge-functions/overview/). If you pass the option `edge: true` to the `adapter` function, server-side rendering will happen in a Deno-based edge function that's deployed close to the site visitor. If set to `false` (the default), the site will deploy to Node-based Netlify Functions. ```js -/// file: svelte.config.js +// @errors: 2307 +/// file: vite.config.js import adapter from '@sveltejs/adapter-netlify'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // will create a Netlify Edge Function using Deno-based - // rather than using standard Node-based functions - edge: true - }) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // will create a Netlify Edge Function using Deno-based + // rather than using standard Node-based functions + edge: true + }) + }) + ] +}); ``` ## Netlify alternatives to SvelteKit functionality diff --git a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md index f9b83326de39..537edefe9983 100644 --- a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md +++ b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md @@ -8,24 +8,28 @@ This adapter will be installed by default when you use [`adapter-auto`](adapter- ## Usage -Install with `npm i -D @sveltejs/adapter-vercel`, then add the adapter to your `svelte.config.js`: +Install with `npm i -D @sveltejs/adapter-vercel`, then add the adapter to your `vite.config.js`: ```js -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-vercel'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // see below for options that can be set here - }) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // see below for options that can be set here + }) + }) + ] +}); ``` +> [!LEGACY] +> The `adapter` option was moved to the SvelteKit Vite plugin in SvelteKit 3.0.0. In earlier versions, you had to add it to the `kit` property in the `svelte.config.js` file instead. + ## Deployment configuration To control how your routes are deployed to Vercel as functions, you can specify deployment configuration, either through the option shown above or with [`export const config`](page-options#config) inside `+server.js`, `+page(.server).js` and `+layout(.server).js` files. @@ -64,24 +68,25 @@ If your functions need to access data in a specific region, it's recommended tha You may set the `images` config to control how Vercel builds your images. See the [image configuration reference](https://vercel.com/docs/build-output-api/v3/configuration#images) for full details. As an example, you may set: ```js -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-vercel'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - images: { - sizes: [640, 828, 1200, 1920, 3840], - formats: ['image/avif', 'image/webp'], - minimumCacheTTL: 300, - domains: ['example-app.vercel.app'], - } - }) - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + images: { + sizes: [640, 828, 1200, 1920, 3840], + formats: ['image/avif', 'image/webp'], + minimumCacheTTL: 300, + domains: ['example-app.vercel.app'], + } + }) + }) + ] +}); ``` ## Incremental Static Regeneration 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 823e2ffea350..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` @@ -39,6 +31,11 @@ export default function (options) { // Return `true` if this adapter supports loading `instrumentation.server.js`. // Return `false if it can't, or throw a descriptive error. } + }, + vite: { + plugins: [ + // add plugins here to integrate with Vite + ] } }; @@ -46,7 +43,7 @@ export default function (options) { } ``` -Of these, `name` and `adapt` are required. `emulate` 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: @@ -61,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/documentation/docs/40-best-practices/07-images.md b/documentation/docs/40-best-practices/07-images.md index 51132b5bced7..fff8548e34b1 100644 --- a/documentation/docs/40-best-practices/07-images.md +++ b/documentation/docs/40-best-practices/07-images.md @@ -39,14 +39,17 @@ npm i -D @sveltejs/enhanced-img Adjust `vite.config.js`: ```js -import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '@sveltejs/adapter-node'; +++import { enhancedImages } from '@sveltejs/enhanced-img';+++ +import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ +++enhancedImages(), // must come before the SvelteKit plugin+++ - sveltekit() + sveltekit({ + adapter: adapter() + }) ] }); ``` diff --git a/documentation/docs/60-appendix/10-faq.md b/documentation/docs/60-appendix/10-faq.md index fd1e71d3d351..40696e6e0983 100644 --- a/documentation/docs/60-appendix/10-faq.md +++ b/documentation/docs/60-appendix/10-faq.md @@ -151,13 +151,15 @@ export function GET({ params, url }) { ## How do I use middleware? -`adapter-node` builds a middleware that you can use with your own server for production mode. In dev, you can add middleware to Vite by using a Vite plugin. For example: +`@sveltejs/adapter-node` builds a middleware that you can use with your own server for production mode. In dev, you can add middleware to Vite by using a Vite plugin. For example: ```js /// file: vite.config.js +import adapter from '@sveltejs/adapter-node'; import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; -/** @type {import('vite').Plugin} */ ++++/** @type {import('vite').Plugin} */ const myPlugin = { name: 'log-request-middleware', configureServer(server) { @@ -166,14 +168,16 @@ const myPlugin = { next(); }); } -}; - -/** @type {import('vite').UserConfig} */ -const config = { - plugins: [myPlugin, sveltekit()] -}; - -export default config; +};+++ + +export default defineConfig({ + plugins: [ + +++myPlugin,+++ + sveltekit({ + adapter: adapter() + }) + ] +}); ``` See [Vite's `configureServer` docs](https://vitejs.dev/guide/api-plugin.html#configureserver) for more details including how to control ordering. diff --git a/documentation/docs/98-reference/50-configuration.md b/documentation/docs/98-reference/50-configuration.md index ab9e48e7841f..a95acb6c4cea 100644 --- a/documentation/docs/98-reference/50-configuration.md +++ b/documentation/docs/98-reference/50-configuration.md @@ -6,26 +6,17 @@ Your project's configuration lives in a `svelte.config.js` file at the root of y ```js /// file: svelte.config.js -// @filename: ambient.d.ts -declare module '@sveltejs/adapter-auto' { - const plugin: () => import('@sveltejs/kit').Adapter; - export default plugin; -} - -// @filename: index.js -// ---cut--- -import adapter from '@sveltejs/adapter-auto'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter() - } + kit: { } }; export default config; ``` +> [!LEGACY] +> The `adapter` option was moved to the SvelteKit Vite plugin in SvelteKit 3.0.0. In earlier versions, you had to add it to the `kit` property in the `svelte.config.js` file instead. + ## Config > TYPES: Configuration#Config diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 66c8a2da246e..01186988e4e4 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, @@ -175,41 +175,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 = /** @type {App.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 }; - }; - - /** @type {{ platform: App.Platform, prerender_platform: App.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/pages/svelte.config.js b/packages/adapter-cloudflare/test/apps/pages/svelte.config.js index 20cd2b3ff5b8..821c14379ec8 100644 --- a/packages/adapter-cloudflare/test/apps/pages/svelte.config.js +++ b/packages/adapter-cloudflare/test/apps/pages/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter() - } + kit: {} }; export default config; diff --git a/packages/adapter-cloudflare/test/apps/pages/vite.config.js b/packages/adapter-cloudflare/test/apps/pages/vite.config.js index 29ad08debe6a..01b9947e7d2a 100644 --- a/packages/adapter-cloudflare/test/apps/pages/vite.config.js +++ b/packages/adapter-cloudflare/test/apps/pages/vite.config.js @@ -1,11 +1,12 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../index.js'; /** @type {import('vite').UserConfig} */ const config = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [sveltekit({ adapter: adapter() })] }; export default config; diff --git a/packages/adapter-cloudflare/test/apps/workers/svelte.config.js b/packages/adapter-cloudflare/test/apps/workers/svelte.config.js index 26cd6a965908..821c14379ec8 100644 --- a/packages/adapter-cloudflare/test/apps/workers/svelte.config.js +++ b/packages/adapter-cloudflare/test/apps/workers/svelte.config.js @@ -1,12 +1,6 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter({ - config: 'config/wrangler.jsonc' - }) - } + kit: {} }; export default config; 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/apps/workers/vite.config.js b/packages/adapter-cloudflare/test/apps/workers/vite.config.js index 29ad08debe6a..195f6c80776e 100644 --- a/packages/adapter-cloudflare/test/apps/workers/vite.config.js +++ b/packages/adapter-cloudflare/test/apps/workers/vite.config.js @@ -1,11 +1,18 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../index.js'; /** @type {import('vite').UserConfig} */ const config = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter({ + config: 'config/wrangler.jsonc' + }) + }) + ] }; export default config; 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/adapter-netlify/test/apps/basic/svelte.config.js b/packages/adapter-netlify/test/apps/basic/svelte.config.js index 20cd2b3ff5b8..821c14379ec8 100644 --- a/packages/adapter-netlify/test/apps/basic/svelte.config.js +++ b/packages/adapter-netlify/test/apps/basic/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter() - } + kit: {} }; export default config; diff --git a/packages/adapter-netlify/test/apps/basic/vite.config.ts b/packages/adapter-netlify/test/apps/basic/vite.config.ts index 72a307cc0e56..8c3bc9c5e4a7 100644 --- a/packages/adapter-netlify/test/apps/basic/vite.config.ts +++ b/packages/adapter-netlify/test/apps/basic/vite.config.ts @@ -1,11 +1,16 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; +import adapter from '../../../index.js'; const config: UserConfig = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] }; export default config; diff --git a/packages/adapter-netlify/test/apps/edge/svelte.config.js b/packages/adapter-netlify/test/apps/edge/svelte.config.js index c633196b8ea8..821c14379ec8 100644 --- a/packages/adapter-netlify/test/apps/edge/svelte.config.js +++ b/packages/adapter-netlify/test/apps/edge/svelte.config.js @@ -1,12 +1,6 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter({ - edge: true - }) - } + kit: {} }; export default config; diff --git a/packages/adapter-netlify/test/apps/edge/vite.config.ts b/packages/adapter-netlify/test/apps/edge/vite.config.ts index 72a307cc0e56..8d97a74850b9 100644 --- a/packages/adapter-netlify/test/apps/edge/vite.config.ts +++ b/packages/adapter-netlify/test/apps/edge/vite.config.ts @@ -1,11 +1,18 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; +import adapter from '../../../index.js'; const config: UserConfig = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter({ + edge: true + }) + }) + ] }; export default config; diff --git a/packages/adapter-netlify/test/apps/instrumentation/svelte.config.js b/packages/adapter-netlify/test/apps/instrumentation/svelte.config.js index 050579db13ba..02c1e4e94183 100644 --- a/packages/adapter-netlify/test/apps/instrumentation/svelte.config.js +++ b/packages/adapter-netlify/test/apps/instrumentation/svelte.config.js @@ -1,9 +1,6 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter(), experimental: { instrumentation: { server: true diff --git a/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts b/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts index 72a307cc0e56..8c3bc9c5e4a7 100644 --- a/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts +++ b/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts @@ -1,11 +1,16 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; +import adapter from '../../../index.js'; const config: UserConfig = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] }; export default config; diff --git a/packages/adapter-netlify/test/apps/split/svelte.config.js b/packages/adapter-netlify/test/apps/split/svelte.config.js index 4bbe888fd1cf..817268bbddb1 100644 --- a/packages/adapter-netlify/test/apps/split/svelte.config.js +++ b/packages/adapter-netlify/test/apps/split/svelte.config.js @@ -1,10 +1,7 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { compilerOptions: { experimental: { async: true } }, kit: { - adapter: adapter({ split: true }), experimental: { remoteFunctions: true } diff --git a/packages/adapter-netlify/test/apps/split/vite.config.ts b/packages/adapter-netlify/test/apps/split/vite.config.ts index 72a307cc0e56..f6fb37e73b3e 100644 --- a/packages/adapter-netlify/test/apps/split/vite.config.ts +++ b/packages/adapter-netlify/test/apps/split/vite.config.ts @@ -1,11 +1,16 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; +import adapter from '../../../index.js'; const config: UserConfig = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter({ split: true }) + }) + ] }; export default config; diff --git a/packages/adapter-static/test/apps/prerendered/package.json b/packages/adapter-static/test/apps/prerendered/package.json index 6ac8a9517693..e2b887afe266 100644 --- a/packages/adapter-static/test/apps/prerendered/package.json +++ b/packages/adapter-static/test/apps/prerendered/package.json @@ -1,5 +1,5 @@ { - "name": "~TODO~", + "name": "test-static-prerendered", "version": "0.0.1", "private": true, "scripts": { diff --git a/packages/adapter-static/test/apps/prerendered/svelte.config.js b/packages/adapter-static/test/apps/prerendered/svelte.config.js index 20cd2b3ff5b8..821c14379ec8 100644 --- a/packages/adapter-static/test/apps/prerendered/svelte.config.js +++ b/packages/adapter-static/test/apps/prerendered/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter() - } + kit: {} }; export default config; diff --git a/packages/adapter-static/test/apps/prerendered/vite.config.js b/packages/adapter-static/test/apps/prerendered/vite.config.js index 29ad08debe6a..dc6532d9578d 100644 --- a/packages/adapter-static/test/apps/prerendered/vite.config.js +++ b/packages/adapter-static/test/apps/prerendered/vite.config.js @@ -1,11 +1,16 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../index.js'; /** @type {import('vite').UserConfig} */ const config = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] }; export default config; diff --git a/packages/adapter-static/test/apps/spa/package.json b/packages/adapter-static/test/apps/spa/package.json index 4d4d0a6f5123..7860e3591bd1 100644 --- a/packages/adapter-static/test/apps/spa/package.json +++ b/packages/adapter-static/test/apps/spa/package.json @@ -1,5 +1,5 @@ { - "name": "~TODO~", + "name": "test-static-spa", "version": "0.0.1", "private": true, "scripts": { diff --git a/packages/adapter-static/test/apps/spa/svelte.config.js b/packages/adapter-static/test/apps/spa/svelte.config.js index da388bfc49d9..821c14379ec8 100644 --- a/packages/adapter-static/test/apps/spa/svelte.config.js +++ b/packages/adapter-static/test/apps/spa/svelte.config.js @@ -1,12 +1,6 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter({ - fallback: '200.html' - }) - } + kit: {} }; export default config; diff --git a/packages/adapter-static/test/apps/spa/vite.config.js b/packages/adapter-static/test/apps/spa/vite.config.js index 29ad08debe6a..7e81282e251e 100644 --- a/packages/adapter-static/test/apps/spa/vite.config.js +++ b/packages/adapter-static/test/apps/spa/vite.config.js @@ -1,11 +1,18 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../index.js'; /** @type {import('vite').UserConfig} */ const config = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter({ + fallback: '200.html' + }) + }) + ] }; export default config; diff --git a/packages/adapter-vercel/test/apps/basic/svelte.config.js b/packages/adapter-vercel/test/apps/basic/svelte.config.js index 20cd2b3ff5b8..821c14379ec8 100644 --- a/packages/adapter-vercel/test/apps/basic/svelte.config.js +++ b/packages/adapter-vercel/test/apps/basic/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter() - } + kit: {} }; export default config; diff --git a/packages/adapter-vercel/test/apps/basic/vite.config.ts b/packages/adapter-vercel/test/apps/basic/vite.config.ts index bbf8c7da43f0..30a15a1a0fd0 100644 --- a/packages/adapter-vercel/test/apps/basic/vite.config.ts +++ b/packages/adapter-vercel/test/apps/basic/vite.config.ts @@ -1,6 +1,11 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; +import adapter from '../../../index.js'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] }); diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index bb5ef66edea7..90e740bcc5fb 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -7,7 +7,7 @@ import { extname, resolve, join, dirname, relative } from 'node:path'; import { pipeline } from 'node:stream'; import { promisify, styleText } from 'node:util'; import zlib from 'node:zlib'; -import { copy, rimraf, mkdirp, posixify } from '../../utils/filesystem.js'; +import { copy, rimraf, mkdirp } from '../../utils/filesystem.js'; import { generate_manifest } from '../generate_manifest/index.js'; import { get_route_segments } from '../../utils/routing.js'; import { get_env } from '../../exports/vite/utils.js'; @@ -16,6 +16,7 @@ import { write } from '../sync/utils.js'; import { list_files } from '../utils.js'; import { find_server_assets } from '../generate_manifest/find_server_assets.js'; import { reserved } from '../env.js'; +import { posixify } from '../../utils/os.js'; const pipe = promisify(pipeline); const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.wasm', '.txt']; @@ -31,7 +32,8 @@ const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.w * prerender_map: PrerenderMap; * log: Logger; * vite_config: ResolvedConfig; - * remotes: RemoteChunk[] + * remotes: RemoteChunk[]; + * out: string; * }} opts * @returns {Builder} */ @@ -44,7 +46,8 @@ export function create_builder({ prerender_map, log, vite_config, - remotes + remotes, + out }) { /** @type {Map} */ const lookup = new Map(); @@ -113,11 +116,10 @@ export function create_builder({ async generateFallback(dest) { const manifest_path = `${config.kit.outDir}/output/server/manifest-full.js`; - const env = get_env(config.kit.env, vite_config.mode); const fallback = await generate_fallback({ manifest_path, - env: { ...env.private, ...env.public }, + out, root: vite_config.root }); diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js index 304a1ba5987d..2e255854ff68 100644 --- a/packages/kit/src/core/adapt/builder.spec.js +++ b/packages/kit/src/core/adapt/builder.spec.js @@ -3,7 +3,7 @@ import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { assert, expect, test } from 'vitest'; import { create_builder } from './builder.js'; -import { posixify } from '../../utils/filesystem.js'; +import { posixify } from '../../utils/os.js'; import { list_files } from '../utils.js'; test('copy files', () => { diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index d50a57e36060..b535dfa29786 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -1,7 +1,9 @@ +/** @import { Adapter } from '@sveltejs/kit' */ import { styleText } from 'node:util'; import { create_builder } from './builder.js'; /** + * @param {Adapter} adapter * @param {import('types').ValidatedConfig} config * @param {import('types').BuildData} build_data * @param {import('types').ServerMetadata} server_metadata @@ -10,8 +12,10 @@ import { create_builder } from './builder.js'; * @param {import('types').Logger} log * @param {import('types').RemoteChunk[]} remotes * @param {import('vite').ResolvedConfig} vite_config + * @param {string} out */ export async function adapt( + adapter, config, build_data, server_metadata, @@ -19,10 +23,11 @@ export async function adapt( prerender_map, log, remotes, - vite_config + vite_config, + out ) { // This is only called when adapter is truthy, so the cast is safe - const { name, adapt } = /** @type {import('@sveltejs/kit').Adapter} */ (config.kit.adapter); + const { name, adapt } = adapter; console.log(styleText(['bold', 'cyan'], `\n> Using ${name}`)); @@ -35,7 +40,8 @@ export async function adapt( prerender_map, log, remotes, - vite_config + vite_config, + out }); await adapt(builder); diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 9b76f9acd23a..d920f47f1d2f 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -55,7 +55,6 @@ const directive_defaults = { const get_defaults = (prefix = '') => ({ extensions: ['.svelte'], kit: { - adapter: null, alias: {}, appDir: '_app', csp: { diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 498f8d58457b..ce9a0ba9307a 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -1,6 +1,7 @@ /** @import { Validator } from './types.js' */ import process from 'node:process'; +import { dedent } from '../sync/utils.js'; const directives = object({ 'child-src': string_array(), @@ -58,19 +59,18 @@ const options = object( }), kit: object({ - adapter: validate(null, (input, keypath) => { - if (typeof input !== 'object' || !input.adapt) { - let message = `${keypath} should be an object with an "adapt" method`; - - if (Array.isArray(input) || typeof input === 'string') { - // for the early adapter adopters - message += ', rather than the name of an adapter'; - } - - throw new Error(`${message}. See https://svelte.dev/docs/kit/adapters`); - } - - return input; + adapter: removed((keypath) => { + return dedent` + ${keypath} has been removed. Instead, pass your adapter to the \`sveltekit\` Vite plugin in the \`vite.config.js\` file. For example: + + import { defineConfig } from 'vite'; + import { sveltekit } from '@sveltejs/kit/vite'; + +++import adapter from '@sveltejs/adapter-auto';+++ + + export default defineConfig({ + plugins: [sveltekit(+++{ adapter: adapter() }+++)] + }); + `; }), alias: validate({}, (input, keypath) => { @@ -356,7 +356,7 @@ function removed( * @param {boolean} [allow_unknown] * @returns {Validator} */ -function object(children, allow_unknown = false) { +export function object(children, allow_unknown = false) { return (input, keypath) => { /** @type {Record} */ const output = {}; @@ -396,7 +396,7 @@ function object(children, allow_unknown = false) { * @param {(value: any, keypath: string) => any} fn * @returns {Validator} */ -function validate(fallback, fn) { +export function validate(fallback, fn) { return (input, keypath) => { return input === undefined ? fallback : fn(input, keypath); }; diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index 4bbc78509bdf..b3b4d47e7f5a 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -104,6 +104,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/ambient.d.ts b/packages/kit/src/core/postbuild/ambient.d.ts new file mode 100644 index 000000000000..d8dcbaeea1f0 --- /dev/null +++ b/packages/kit/src/core/postbuild/ambient.d.ts @@ -0,0 +1,3 @@ +declare module '__SERVER__/index.js' { + export { Server } from '@sveltejs/kit'; +} diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 98ddcca5276d..2002e2873218 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -1,294 +1,64 @@ -/** @import { RemoteChunk } from 'types' */ -import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { validate_server_exports } from '../../utils/exports.js'; -import { load_config } from '../config/index.js'; +/** @import { ManifestData, ServerMetadata } from 'types' */ +/** @import { Manifest } from 'vite' */ +import * as devalue from 'devalue'; import { forked } from '../../utils/fork.js'; -import { ENDPOINT_METHODS } from '../../constants.js'; -import { filter_env } from '../../utils/env.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'; import { build_server_nodes } from '../../exports/vite/build/build_server.js'; +import { create_build_server } from '../../exports/vite/build/vite_server.js'; +import { load_config } from '../config/index.js'; export default forked(import.meta.url, analyse); +const analyse_entry = import.meta.resolve('./analyse_entry.js'); + /** - * @param {{ - * hash: boolean; - * manifest_path: string; - * manifest_data: import('types').ManifestData; - * server_manifest: import('vite').Manifest; - * tracked_features: Record; - * env: Record; - * out: string; - * output_config: import('types').RecursiveRequired; - * remotes: RemoteChunk[]; - * root: string; - * }} opts + * @param {object} opts Arguments must be serialisable via the structured clone algorithm + * @param {string} opts.manifest_path + * @param {ManifestData} opts.manifest_data + * @param {Manifest} opts.server_manifest + * @param {Record} opts.tracked_features + * @param {string} opts.out + * @param {string} opts.root + * @returns {Promise<{ metadata: ServerMetadata }>} */ async function analyse({ - hash, manifest_path, manifest_data, server_manifest, tracked_features, - env, out, - output_config, - remotes, root }) { - /** @type {import('@sveltejs/kit').SSRManifest} */ - const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; - - /** @type {import('types').ValidatedKitConfig} */ - const config = (await load_config({ cwd: root })).kit; - - const server_root = join(config.outDir, 'output'); - - /** @type {import('types').ServerInternalModule} */ - const internal = await import(pathToFileURL(`${server_root}/server/internal.js`).href); - - // configure `import { building } from '$app/environment'` — - // essential we do this before analysing the code - internal.set_building(); - - // set env, `read`, and `manifest`, in case they're used in initialisation - const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env; - const private_env = filter_env(env, private_prefix, public_prefix); - const public_env = filter_env(env, public_prefix, private_prefix); - internal.set_private_env(private_env); - internal.set_public_env(public_env); - internal.set_manifest(manifest); - internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`)); - // first, build server nodes without the client manifest so we can analyse it - build_server_nodes( - out, - config, - manifest_data, - server_manifest, - null, - null, - null, - output_config, - root - ); - - /** @type {import('types').ServerMetadata} */ - const metadata = { - nodes: [], - routes: new Map(), - remotes: new Map() - }; - - const nodes = await Promise.all(manifest._.nodes.map((loader) => loader())); - - // analyse nodes - for (const node of nodes) { - if (hash && node.universal) { - const options = Object.keys(node.universal).filter((o) => o !== 'load'); - if (options.length > 0) { - throw new Error( - `Page options are ignored when \`router.type === 'hash'\` (${node.universal_id} has ${options - .filter((o) => o !== 'load') - .map((o) => `'${o}'`) - .join(', ')})` - ); - } - } - - metadata.nodes[node.index] = { - has_server_load: has_server_load(node), - has_universal_load: node.universal?.load !== undefined - }; - } + build_server_nodes({ out, manifest_data, server_manifest, root }); - // analyse routes - for (const route of manifest._.routes) { - const page = - route.page && - analyse_page( - route.page.layouts.map((n) => (n === undefined ? n : nodes[n])), - nodes[route.page.leaf] - ); - - const endpoint = route.endpoint && analyse_endpoint(route, await route.endpoint()); - - if (page?.prerender && endpoint?.prerender) { - throw new Error(`Cannot prerender a route with both +page and +server files (${route.id})`); - } - - if (page?.config && endpoint?.config) { - for (const key in { ...page.config, ...endpoint.config }) { - if (JSON.stringify(page.config[key]) !== JSON.stringify(endpoint.config[key])) { - throw new Error( - `Mismatched route config for ${route.id} — the +page and +server files must export the same config, if any` - ); - } - } - } - - const route_config = page?.config ?? endpoint?.config ?? {}; - const prerender = page?.prerender ?? endpoint?.prerender; - - if (prerender !== true) { - for (const feature of list_features( - route, - manifest_data, - server_manifest, - tracked_features - )) { - check_feature(route.id, route_config, feature, config.adapter); - } - } - - const page_methods = page?.methods ?? []; - const api_methods = endpoint?.methods ?? []; - const entries = page?.entries ?? endpoint?.entries; - - metadata.routes.set(route.id, { - config: route_config, - methods: Array.from(new Set([...page_methods, ...api_methods])), - page: { - methods: page_methods - }, - api: { - methods: api_methods - }, - prerender, - entries: - entries && (await entries()).map((entry_object) => resolve_route(route.id, entry_object)) - }); - } - - // analyse remotes - for (const remote of remotes) { - const loader = manifest._.remotes[remote.hash]; - const { default: functions } = await loader(); - - const exports = new Map(); - - for (const name in functions) { - const internals = /** @type {import('types').RemoteInternals} */ (functions[name].__); - const type = internals.type; - - exports.set(name, { - type, - dynamic: type !== 'prerender' || internals.dynamic - }); - } - - metadata.remotes.set(remote.hash, exports); - } - - return { metadata }; -} + const svelte_config = await load_config({ cwd: root }); -/** - * @param {import('types').SSRRoute} route - * @param {import('types').SSREndpoint} mod - */ -function analyse_endpoint(route, mod) { - validate_server_exports(mod, route.id); - - if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { - throw new Error( - `Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})` - ); - } - - /** @type {Array} */ - const methods = []; - - for (const method of /** @type {import('types').HttpMethod[]} */ (ENDPOINT_METHODS)) { - if (mod[method]) methods.push(method); - } - - if (mod.fallback) { - methods.push('*'); - } - - return { - config: mod.config, - entries: mod.entries, - methods, - prerender: mod.prerender ?? false - }; -} - -/** - * @param {Array} layouts - * @param {import('types').SSRNode} leaf - */ -function analyse_page(layouts, leaf) { - /** @type {Array<'GET' | 'POST'>} */ - const methods = ['GET']; - if (leaf.server?.actions) methods.push('POST'); - - const nodes = new PageNodes([...layouts, leaf]); - nodes.validate(); - - return { - config: nodes.get_config(), - entries: leaf.universal?.entries ?? leaf.server?.entries, - methods, - prerender: nodes.prerender() - }; -} - -/** - * @param {import('types').SSRRoute} route - * @param {import('types').ManifestData} manifest_data - * @param {import('vite').Manifest} server_manifest - * @param {Record} tracked_features - */ -function list_features(route, manifest_data, server_manifest, tracked_features) { - const features = new Set(); - - const route_data = /** @type {import('types').RouteData} */ ( - manifest_data.routes.find((r) => r.id === route.id) - ); - - const visited = new Set(); - /** @param {string} id */ - function visit(id) { - if (visited.has(id)) return; - visited.add(id); - - const chunk = server_manifest[id]; - if (!chunk) return; - - if (chunk.file in tracked_features) { - for (const feature of tracked_features[chunk.file]) { - features.add(feature); - } - } - - if (chunk.imports) { - for (const id of chunk.imports) { - visit(id); - } - } - } - - let page_node = route_data?.leaf; - while (page_node) { - if (page_node.server) visit(page_node.server); - page_node = page_node.parent ?? null; - } - - if (route_data.endpoint) { - visit(route_data.endpoint.file); - } - - if (manifest_data.hooks.server) { - // TODO if hooks.server.js imports `read`, it will be in the entry chunk - // we don't currently account for that case - visit(manifest_data.hooks.server); - } - - return Array.from(features); + const vite = await create_build_server({ + svelte_config, + out, + manifest_path, + server_path: analyse_entry + }); + + await vite.listen(); + + const address = vite.httpServer?.address(); + const port = typeof address === 'string' ? Number(address.split(':').at(-1)) : address?.port; + + const response = await fetch(new URL(`http://localhost:${port}`), { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: devalue.stringify({ + server_manifest, + tracked_features, + manifest_data, + hash: svelte_config.kit.router.type === 'hash' + }) + }); + + await vite.close(); + + return { metadata: devalue.parse(await response.text()) }; } diff --git a/packages/kit/src/core/postbuild/analyse_entry.js b/packages/kit/src/core/postbuild/analyse_entry.js new file mode 100644 index 000000000000..c1738d8752f4 --- /dev/null +++ b/packages/kit/src/core/postbuild/analyse_entry.js @@ -0,0 +1,313 @@ +/** @import { Server as KitServer, SSRManifest } from '@sveltejs/kit' */ +/** @import { ManifestData, ServerMetadata } from 'types' */ +/** @import { Manifest } from 'vite' */ +import { get } from '__sveltekit/ipc'; +import { + options, + set_manifest, + set_read_implementation, + set_private_env, + set_public_env, + set_building +} from '__SERVER__/internal.js'; +import * as devalue from 'devalue'; +import { ENDPOINT_METHODS } from '../../constants.js'; +import { has_server_load, resolve_route } from '../../utils/routing.js'; +import { validate_server_exports } from '../../utils/exports.js'; +import { PageNodes } from '../../utils/page_nodes.js'; +import { check_feature } from '../../utils/features.js'; +import { filter_env } from '../../utils/env.js'; +import { create_synchronous_read } from '../../runtime/server/read.js'; + +set_building(); + +/** @implements {KitServer} */ +export class Server { + /** @type {import('@sveltejs/kit').SSRManifest} */ + manifest; + + /** @type {import('types').SSROptions} */ + options; + + // set env, `read`, and `manifest`, in case they're used when we analyse the user's code + + /** @param {SSRManifest} manifest */ + constructor(manifest) { + this.manifest = manifest; + /** @type {import('types').SSROptions} */ + this.options = options; + + set_manifest(manifest); + } + + /** @type {KitServer['init']} */ + init({ env }) { + const { env_public_prefix, env_private_prefix } = this.options; + + set_private_env(filter_env(env, env_private_prefix, env_public_prefix)); + set_public_env(filter_env(env, env_public_prefix, env_private_prefix)); + + set_read_implementation( + create_synchronous_read(async (file) => { + const response = await get(`/read?${new URLSearchParams({ file })}`); + if (!response.ok) { + throw new Error( + `read(...) failed: could not fetch ${file} (${response.status} ${response.statusText})` + ); + } + return response.body; + }) + ); + + return Promise.resolve(); + } + + /** @type {KitServer['respond']} */ + async respond(request) { + /** @type {{ hash: boolean; server_manifest: Manifest; tracked_features: Record; manifest_data: ManifestData; }} */ + const { server_manifest, tracked_features, manifest_data, hash } = devalue.parse( + await request.text() + ); + + const metadata = await analyse({ + server_manifest, + tracked_features, + manifest: this.manifest, + manifest_data, + hash + }); + + return new Response(devalue.stringify(metadata)); + } +} + +/** + * + * @param {object} opts + * @param {Manifest} opts.server_manifest + * @param {Record} opts.tracked_features + * @param {SSRManifest} opts.manifest + * @param {ManifestData} opts.manifest_data + * @param {boolean} opts.hash + * @returns {Promise} + */ +async function analyse({ server_manifest, tracked_features, manifest, manifest_data, hash }) { + /** @type {import('types').ServerMetadata} */ + const metadata = { + nodes: [], + routes: new Map(), + remotes: new Map(), + remotes_with_prerender: new Set() + }; + + const nodes = await Promise.all(manifest._.nodes.map((loader) => loader())); + + // analyse nodes + for (const node of nodes) { + if (hash && node.universal) { + const options = Object.keys(node.universal).filter((o) => o !== 'load'); + if (options.length > 0) { + throw new Error( + `Page options are ignored when \`router.type === 'hash'\` (${node.universal_id} has ${options + .filter((o) => o !== 'load') + .map((o) => `'${o}'`) + .join(', ')})` + ); + } + } + + metadata.nodes[node.index] = { + has_server_load: has_server_load(node), + has_universal_load: node.universal?.load !== undefined + }; + } + + // analyse routes + for (const route of manifest._.routes) { + const page = + route.page && + analyse_page( + route.page.layouts.map((n) => (n === undefined ? n : nodes[n])), + nodes[route.page.leaf] + ); + + const endpoint = route.endpoint && analyse_endpoint(route, await route.endpoint()); + + if (page?.prerender && endpoint?.prerender) { + throw new Error(`Cannot prerender a route with both +page and +server files (${route.id})`); + } + + if (page?.config && endpoint?.config) { + for (const key in { ...page.config, ...endpoint.config }) { + if (JSON.stringify(page.config[key]) !== JSON.stringify(endpoint.config[key])) { + throw new Error( + `Mismatched route config for ${route.id} — the +page and +server files must export the same config, if any` + ); + } + } + } + + const route_config = page?.config ?? endpoint?.config ?? {}; + const prerender = page?.prerender ?? endpoint?.prerender; + + if (prerender !== true) { + for (const feature of list_features( + route, + manifest_data, + server_manifest, + tracked_features + )) { + await check_feature(route.id, route_config, feature); + } + } + + const page_methods = page?.methods ?? []; + const api_methods = endpoint?.methods ?? []; + const entries = page?.entries ?? endpoint?.entries; + + metadata.routes.set(route.id, { + config: route_config, + methods: Array.from(new Set([...page_methods, ...api_methods])), + page: { + methods: page_methods + }, + api: { + methods: api_methods + }, + prerender, + entries: + entries && (await entries()).map((entry_object) => resolve_route(route.id, entry_object)) + }); + } + + // analyse remotes + for (const [hash, loader] of Object.entries(manifest._.remotes)) { + const { default: functions } = await loader(); + + const exports = new Map(); + + for (const name in functions) { + const internals = /** @type {import('types').RemoteInternals} */ (functions[name].__); + const type = internals.type; + + exports.set(name, { + type, + dynamic: type !== 'prerender' || internals.dynamic + }); + + if (type === 'prerender') { + metadata.remotes_with_prerender.add(hash); + } + } + + metadata.remotes.set(hash, exports); + } + + return metadata; +} + +/** + * @param {import('types').SSRRoute} route + * @param {import('types').SSREndpoint} mod + */ +function analyse_endpoint(route, mod) { + validate_server_exports(mod, route.id); + + if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { + throw new Error( + `Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})` + ); + } + + /** @type {Array} */ + const methods = []; + + for (const method of /** @type {import('types').HttpMethod[]} */ (ENDPOINT_METHODS)) { + if (mod[method]) methods.push(method); + } + + if (mod.fallback) { + methods.push('*'); + } + + return { + config: mod.config, + entries: mod.entries, + methods, + prerender: mod.prerender ?? false + }; +} + +/** + * @param {Array} layouts + * @param {import('types').SSRNode} leaf + */ +function analyse_page(layouts, leaf) { + /** @type {Array<'GET' | 'POST'>} */ + const methods = ['GET']; + if (leaf.server?.actions) methods.push('POST'); + + const nodes = new PageNodes([...layouts, leaf]); + nodes.validate(); + + return { + config: nodes.get_config(), + entries: leaf.universal?.entries ?? leaf.server?.entries, + methods, + prerender: nodes.prerender() + }; +} + +/** + * @param {import('types').SSRRoute} route + * @param {import('types').ManifestData} manifest_data + * @param {import('vite').Manifest} server_manifest + * @param {Record} tracked_features + */ +function list_features(route, manifest_data, server_manifest, tracked_features) { + const features = new Set(); + + const route_data = /** @type {import('types').RouteData} */ ( + manifest_data.routes.find((r) => r.id === route.id) + ); + + const visited = new Set(); + /** @param {string} id */ + function visit(id) { + if (visited.has(id)) return; + visited.add(id); + + const chunk = server_manifest[id]; + if (!chunk) return; + + if (chunk.file in tracked_features) { + for (const feature of tracked_features[chunk.file]) { + features.add(feature); + } + } + + if (chunk.imports) { + for (const id of chunk.imports) { + visit(id); + } + } + } + + let page_node = route_data?.leaf; + while (page_node) { + if (page_node.server) visit(page_node.server); + page_node = page_node.parent ?? null; + } + + if (route_data.endpoint) { + visit(route_data.endpoint.file); + } + + if (manifest_data.hooks.server) { + // TODO if hooks.server.js imports `read`, it will be in the entry chunk + // we don't currently account for that case + visit(manifest_data.hooks.server); + } + + return Array.from(features); +} diff --git a/packages/kit/src/core/postbuild/fallback.js b/packages/kit/src/core/postbuild/fallback.js index 66fa0c6379e7..1421875985fd 100644 --- a/packages/kit/src/core/postbuild/fallback.js +++ b/packages/kit/src/core/postbuild/fallback.js @@ -1,49 +1,56 @@ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; +/** @import { PluginOption } from 'vite' */ +import { escape_for_regexp } from '../../utils/escape.js'; +import { create_build_server } from '../../exports/vite/build/vite_server.js'; import { load_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; export default forked(import.meta.url, generate_fallback); +const prerender_entry = import.meta.resolve('./prerender_entry.js'); + /** - * @param {{ - * manifest_path: string; - * env: Record; - * root: string; - * }} opts + * @param {object} opts Arguments must be serialisable via the structured clone algorithm + * @param {string} opts.manifest_path + * @param {string} opts.out + * @param {string} opts.root + * @returns {Promise} */ -async function generate_fallback({ manifest_path, env, root }) { - /** @type {import('types').ValidatedKitConfig} */ - const config = (await load_config({ cwd: root })).kit; - - const server_root = join(config.outDir, 'output'); - - /** @type {import('types').ServerInternalModule} */ - const { set_building } = await import(pathToFileURL(`${server_root}/server/internal.js`).href); - - /** @type {import('types').ServerModule} */ - const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).href); - - /** @type {import('@sveltejs/kit').SSRManifest} */ - const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; +async function generate_fallback({ manifest_path, out, root }) { + const svelte_config = await load_config({ cwd: root }); + + /** @type {PluginOption} */ + const plugin_generate_fallback = { + name: 'vite-plugin-sveltekit-compile:generate-fallback', + configureServer(vite) { + return () => { + vite.middlewares.use((req, _, next) => { + req.url = req.url?.replace( + new RegExp(escape_for_regexp(`^http://localhost:${port}`)), + svelte_config.kit.prerender.origin + ); + req.headers.host = new URL(svelte_config.kit.prerender.origin).host; + + next(); + }); + }; + } + }; + + const vite = await create_build_server({ + svelte_config, + out, + manifest_path, + server_path: prerender_entry, + vite_plugins: [plugin_generate_fallback] + }); - set_building(); + await vite.listen(); - const server = new Server(manifest); - await server.init({ env }); + const address = vite.httpServer?.address(); + const port = typeof address === 'string' ? Number(address.split(':').at(-1)) : address?.port; + const response = await fetch(`http://localhost:${port}/[fallback]`); - const response = await server.respond(new Request(config.prerender.origin + '/[fallback]'), { - getClientAddress: () => { - throw new Error('Cannot read clientAddress during prerendering'); - }, - prerendering: { - fallback: true, - dependencies: new Map(), - remote_responses: new Map() - }, - read: (file) => readFileSync(join(config.files.assets, file)) - }); + await vite.close(); if (response.ok) { return await response.text(); diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 79d5fd0b8c2e..a64ecd244acb 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -1,21 +1,23 @@ +/** @import { Logger, PrerenderDependency, Prerendered, PrerenderMap, ServerMetadata } from 'types' */ +/** @import { PluginOption } from 'vite' */ +/** @import { SerialisedResponse } from '../../exports/vite/types.js' */ import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { mkdirp, posixify, walk } from '../../utils/filesystem.js'; +import { mkdirp, walk } from '../../utils/filesystem.js'; import { noop } from '../../utils/functions.js'; import { decode_uri, is_root_relative, resolve } from '../../utils/url.js'; -import { escape_html } from '../../utils/escape.js'; +import { escape_for_regexp, escape_html } from '../../utils/escape.js'; import { logger } from '../utils.js'; -import { load_config } from '../config/index.js'; import { get_route_segments } from '../../utils/routing.js'; import { queue } from './queue.js'; import { crawl } from './crawl.js'; import { forked } from '../../utils/fork.js'; import * as devalue from 'devalue'; -import { createReadableStream } from '@sveltejs/kit/node'; import generate_fallback from './fallback.js'; -import { stringify_remote_arg } from '../../runtime/shared.js'; -import { filter_env } from '../../utils/env.js'; +import { posixify } from '../../utils/os.js'; +import { create_app_dir_matcher } from '../../exports/vite/dev/index.js'; +import { create_build_server } from '../../exports/vite/build/vite_server.js'; +import { load_config } from '../config/index.js'; export default forked(import.meta.url, prerender); @@ -25,36 +27,21 @@ export default forked(import.meta.url, prerender); // "If decodedFragment is an ASCII case-insensitive match for the string 'top', then return the top of the document." const SPECIAL_HASHLINKS = new Set(['', 'top']); +const prerender_entry = import.meta.resolve('./prerender_entry.js'); + /** - * @param {{ - * hash: boolean; - * out: string; - * manifest_path: string; - * metadata: import('types').ServerMetadata; - * verbose: boolean; - * env: Record; - * root: string; - * }} opts + * @param {object} opts Arguments must be serialisable via the structured clone algorithm + * @param {string} opts.out + * @param {string} opts.manifest_path + * @param {ServerMetadata} opts.metadata + * @param {boolean} opts.verbose + * @param {string} opts.root */ -async function prerender({ hash, out, manifest_path, metadata, verbose, env, root }) { - /** @type {import('@sveltejs/kit').SSRManifest} */ - const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; - - /** @type {import('types').ServerInternalModule} */ - const internal = await import(pathToFileURL(`${out}/server/internal.js`).href); - - /** @type {import('types').ServerModule} */ - const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href); - - // configure `import { building } from '$app/environment'` — - // essential we do this before analysing the code - internal.set_building(); - internal.set_prerendering(); - +async function prerender({ out, manifest_path, metadata, verbose, root }) { /** * @template {{message: string}} T * @template {Omit} K - * @param {import('types').Logger} log + * @param {Logger} log * @param {'fail' | 'warn' | 'ignore' | ((details: T) => void)} input * @param {(details: K) => string} format * @returns {(details: K) => void} @@ -80,7 +67,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo const OK = 2; const REDIRECT = 3; - /** @type {import('types').Prerendered} */ + /** @type {Prerendered} */ const prerendered = { pages: new Map(), assets: new Map(), @@ -88,7 +75,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo paths: [] }; - /** @type {import('types').PrerenderMap} */ + /** @type {PrerenderMap} */ const prerender_map = new Map(); for (const [id, { prerender }] of metadata.routes) { @@ -97,21 +84,17 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo } } - /** @type {Set} */ - const prerendered_routes = new Set(); - - /** @type {import('types').ValidatedKitConfig} */ - const config = (await load_config({ cwd: root })).kit; + const svelte_config = await load_config({ cwd: root }); - if (hash) { + if (svelte_config.kit.router.type === 'hash') { const fallback = await generate_fallback({ manifest_path, - env, + out, root }); const file = output_filename('/', true); - const dest = `${config.outDir}/output/prerendered/pages/${file}`; + const dest = `${out}/prerendered/pages/${file}`; mkdirp(dirname(dest)); writeFileSync(dest, fallback); @@ -121,9 +104,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo return { prerendered, prerender_map }; } - const emulator = await config.adapter?.emulate?.(); - - /** @type {import('types').Logger} */ + /** @type {Logger} */ const log = logger({ verbose }); /** @type {Map} */ @@ -131,10 +112,10 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo const handle_http_error = normalise_error_handler( log, - config.prerender.handleHttpError, + svelte_config.kit.prerender.handleHttpError, ({ status, path, referrer, referenceType }) => { const message = - status === 404 && !path.startsWith(config.paths.base) + status === 404 && !path.startsWith(svelte_config.kit.paths.base) ? `${path} does not begin with \`base\`, which is configured in \`paths.base\` and can be imported from \`$app/paths\` - see https://svelte.dev/docs/kit/configuration#paths for more info` : path; @@ -144,7 +125,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo const handle_missing_id = normalise_error_handler( log, - config.prerender.handleMissingId, + svelte_config.kit.prerender.handleMissingId, ({ path, id, referrers }) => { return ( `The following pages contain links to ${path}#${id}, but no element with id="${id}" exists on ${path} - see the \`handleMissingId\` option in https://svelte.dev/docs/kit/configuration#prerender for more info:` + @@ -155,7 +136,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo const handle_entry_generator_mismatch = normalise_error_handler( log, - config.prerender.handleEntryGeneratorMismatch, + svelte_config.kit.prerender.handleEntryGeneratorMismatch, ({ generatedFromId, entry, matchedId }) => { return `The entries export from ${generatedFromId} generated entry ${entry}, which was matched by ${matchedId} - see the \`handleEntryGeneratorMismatch\` option in https://svelte.dev/docs/kit/configuration#prerender for more info.`; } @@ -163,21 +144,21 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo const handle_not_prerendered_route = normalise_error_handler( log, - config.prerender.handleUnseenRoutes, + svelte_config.kit.prerender.handleUnseenRoutes, ({ routes }) => { const list = routes.map((id) => ` - ${id}`).join('\n'); return `The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:\n${list}\n\nSee the \`handleUnseenRoutes\` option in https://svelte.dev/docs/kit/configuration#prerender for more info.`; } ); - const q = queue(config.prerender.concurrency); + const q = queue(svelte_config.kit.prerender.concurrency); /** * @param {string} path * @param {boolean} is_html */ function output_filename(path, is_html) { - const file = path.slice(config.paths.base.length + 1) || 'index.html'; + const file = path.slice(svelte_config.kit.paths.base.length + 1) || 'index.html'; if (is_html && !file.endsWith('.html')) { return file + (file.endsWith('/') ? 'index.html' : '.html'); @@ -187,20 +168,19 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo } const files = new Set(walk(`${out}/client`).map(posixify)); - files.add(`${config.appDir}/env.js`); + files.add(`${svelte_config.kit.appDir}/env.js`); - const immutable = `${config.appDir}/immutable`; + const immutable = `${svelte_config.kit.appDir}/immutable`; if (existsSync(`${out}/server/${immutable}`)) { for (const file of walk(`${out}/server/${immutable}`)) { - files.add(posixify(`${config.appDir}/immutable/${file}`)); + files.add(posixify(`${svelte_config.kit.appDir}/immutable/${file}`)); } } - const remote_prefix = `${config.paths.base}/${config.appDir}/remote/`; + const remote_prefix = `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}/remote/`; const seen = new Set(); const written = new Set(); - const remote_responses = new Map(); /** @type {Map>} */ const expected_hashlinks = new Map(); @@ -218,7 +198,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo if (seen.has(decoded)) return; seen.add(decoded); - const file = decoded.slice(config.paths.base.length + 1); + const file = decoded.slice(svelte_config.kit.paths.base.length + 1); if (files.has(file)) return; return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id)); @@ -231,37 +211,35 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo * @param {string} [generated_from_id] */ async function visit(decoded, encoded, referrer, generated_from_id) { - if (!decoded.startsWith(config.paths.base)) { + if (!decoded.startsWith(svelte_config.kit.paths.base)) { handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' }); return; } - /** @type {Map} */ - const dependencies = new Map(); - - const response = await server.respond(new Request(config.prerender.origin + encoded), { - getClientAddress() { - throw new Error('Cannot read clientAddress during prerendering'); - }, - prerendering: { - dependencies, - remote_responses - }, - read: (file) => { - // stuff we just wrote - const filepath = saved.get(file); - if (filepath) return readFileSync(filepath); - - // Static assets emitted during build - if (file.startsWith(config.appDir)) { - return readFileSync(`${out}/server/${file}`); - } + /** @type {PromiseWithResolvers>} */ + const prerender_dependencies = Promise.withResolvers(); + + const event = `sveltekit:prerender-dependencies:${encoded}`; + /** @param {{ dependencies: Record }} data */ + const handle_dependencies = (data) => { + /** @type {Map} */ + const deserialised = new Map(); + for (const [path, dependency] of Object.entries(data.dependencies)) { + deserialised.set(path, { + response: new Response(dependency.response.body, { + headers: dependency.response.headers, + status: dependency.response.status, + statusText: dependency.response.statusText + }), + body: dependency.body + }); + } + prerender_dependencies.resolve(deserialised); + vite.environments.ssr.hot.off(event, handle_dependencies); + }; + vite.environments.ssr.hot.on(event, handle_dependencies); - // stuff in `static` - return readFileSync(join(config.files.assets, file)); - }, - emulator - }); + const response = await fetch(`http://localhost:${port}${encoded}`, { redirect: 'manual' }); const encoded_id = response.headers.get('x-sveltekit-routeid'); const decoded_id = encoded_id && decode_uri(encoded_id); @@ -282,7 +260,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo const category = decoded.startsWith(remote_prefix) ? 'data' : 'pages'; save(category, response, body, decoded, encoded, referrer, 'linked'); - for (const [dependency_path, result] of dependencies) { + for (const [dependency_path, result] of await prerender_dependencies.promise) { // this seems circuitous, but using new URL allows us to not care // whether dependency_path is encoded or not const encoded_dependency_path = new URL(dependency_path, 'http://localhost').pathname; @@ -321,17 +299,21 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo const headers = Object.fromEntries(response.headers); // if it's a 200 HTML response, crawl it. Skip error responses, as we don't save those - if (response.ok && config.prerender.crawl && headers['content-type'] === 'text/html') { + if ( + response.ok && + svelte_config.kit.prerender.crawl && + headers['content-type'] === 'text/html' + ) { const { ids, hrefs } = crawl(body.toString(), decoded); actual_hashlinks.set(decoded, ids); /** @param {string} href */ const removePrerenderOrigin = (href) => { - if (href.startsWith(config.prerender.origin)) { - if (href === config.prerender.origin) return '/'; - if (href.at(config.prerender.origin.length) !== '/') return href; - return href.slice(config.prerender.origin.length); + if (href.startsWith(svelte_config.kit.prerender.origin)) { + if (href === svelte_config.kit.prerender.origin) return '/'; + if (href.at(svelte_config.kit.prerender.origin.length) !== '/') return href; + return href.slice(svelte_config.kit.prerender.origin.length); } return href; }; @@ -360,6 +342,9 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo } } + /** @type {Set} */ + const prerendered_routes = new Set(); + /** * @param {'pages' | 'dependencies' | 'data'} category * @param {Response} response @@ -377,7 +362,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo const is_html = response_type === REDIRECT || type === 'text/html'; const file = output_filename(decoded, is_html); - const dest = `${config.outDir}/output/prerendered/${category}/${file}`; + const dest = `${out}/prerendered/${category}/${file}`; if (written.has(file)) return; @@ -464,7 +449,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo handle_http_error({ status: response.status, path: decoded, referrer, referenceType }); } - manifest.assets.add(file); + vite.environments.ssr.hot.send('sveltekit:prerender-assets', file); saved.set(file, dest); } @@ -476,55 +461,91 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo } } - let should_prerender = false; - - for (const value of prerender_map.values()) { - if (value) { - should_prerender = true; - break; - } - } - - // the user's remote function modules may reference environment variables, - // `read` or the `manifest` at the top-level so we need to set them before - // evaluating those modules to avoid potential runtime errors - const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env; - const private_env = filter_env(env, private_prefix, public_prefix); - const public_env = filter_env(env, public_prefix, private_prefix); - internal.set_private_env(private_env); - internal.set_public_env(public_env); - internal.set_manifest(manifest); - internal.set_read_implementation((file) => createReadableStream(`${out}/server/${file}`)); - - /** @type {Array} */ - const prerender_functions = []; - - for (const loader of Object.values(manifest._.remotes)) { - const module = await loader(); - - for (const fn of Object.values(module.default)) { - if (fn?.__?.type === 'prerender') { - prerender_functions.push(fn.__); - should_prerender = true; - } - } - } + const should_prerender = + prerender_map.values().some((value) => !!value) || !!metadata.remotes_with_prerender.size; if (!should_prerender) { return { prerendered, prerender_map }; } - // only run the server after the `should_prerender` check so that we - // don't run the user's init hook unnecessarily - const server = new Server(manifest); - await server.init({ - env, - read: (file) => createReadableStream(`${config.outDir}/output/server/${file}`) + log.info('Prerendering'); + + const prerender_read_pathname = create_app_dir_matcher( + svelte_config.kit.paths.base, + svelte_config.kit.appDir, + '/prerender-read' + ); + + /** @type {PluginOption} */ + const plugin_prerender = { + name: 'vite-plugin-sveltekit-compile:prerender', + configureServer(vite) { + return () => { + vite.middlewares.use((req, res, next) => { + req.url = req.url?.replace( + new RegExp(escape_for_regexp(`^http://localhost:${port}`)), + svelte_config.kit.prerender.origin + ); + req.headers.host = new URL(svelte_config.kit.prerender.origin).host; + + const base = `${vite.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + const url = new URL(base + req.url); + const decoded = decodeURI(url.pathname); + + if (decoded.match(prerender_read_pathname)) { + const file = url.searchParams.get('file'); + + if (!file) { + res.writeHead(400); + res.end('Missing file query argument'); + return; + } + + /** @type {Buffer} */ + let data; + + // stuff we just wrote + const filepath = saved.get(file); + if (filepath) { + data = readFileSync(filepath); + } else if (file.startsWith(svelte_config.kit.appDir)) { + // Static assets emitted during build + data = readFileSync(`${out}/server/${file}`); + } else { + // stuff in `static` + data = readFileSync(join(svelte_config.kit.files.assets, file)); + } + + res.setHeader('content-type', 'application/octet-stream'); + res.end(data); + return; + } + + next(); + }); + }; + } + }; + + const vite = await create_build_server({ + svelte_config, + out, + manifest_path, + server_path: prerender_entry, + vite_plugins: [plugin_prerender] }); - log.info('Prerendering'); + // only start the app server after checking if prerendering is needed so + // that we don't run the user's `init` hook unnecessarily + await vite.listen(); + + const address = vite.httpServer?.address(); + const port = typeof address === 'string' ? Number(address.split(':').at(-1)) : address?.port; - for (const entry of config.prerender.entries) { + for (const entry of svelte_config.kit.prerender.entries) { if (entry === '*') { for (const [id, prerender] of prerender_map) { if (prerender) { @@ -534,36 +555,40 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, roo if (processed_id.includes('[')) continue; const path = `/${get_route_segments(processed_id).join('/')}`; - void enqueue(null, config.paths.base + path); + void enqueue(null, svelte_config.kit.paths.base + path); } } } else { - void enqueue(null, config.paths.base + entry); + void enqueue(null, svelte_config.kit.paths.base + entry); } } for (const { id, entries } of route_level_entries) { for (const entry of entries) { - void enqueue(null, config.paths.base + entry, undefined, id); + void enqueue(null, svelte_config.kit.paths.base + entry, undefined, id); } } - const transport = (await internal.get_hooks()).transport ?? {}; - for (const internals of prerender_functions) { - if (internals.has_arg) { - for (const arg of (await internals.inputs?.()) ?? []) { - void enqueue( - null, - remote_prefix + internals.id + '/' + stringify_remote_arg(arg, transport) - ); - } - } else { - void enqueue(null, remote_prefix + internals.id); - } + const url = new URL( + `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}/prerender-functions`, + `http://localhost:${port}` + ); + for (const name of metadata.remotes_with_prerender) { + url.searchParams.append('name', name); + } + + const response = await fetch(url); + /** @type {string[]} */ + const functions_to_prerender = await response.json(); + + for (const decoded of functions_to_prerender) { + void enqueue(null, decoded); } await q.done(); + await vite.close(); + // handle invalid fragment links for (const [key, referrers] of expected_hashlinks) { const index = key.indexOf('#'); diff --git a/packages/kit/src/core/postbuild/prerender_entry.js b/packages/kit/src/core/postbuild/prerender_entry.js new file mode 100644 index 000000000000..84622fff17cc --- /dev/null +++ b/packages/kit/src/core/postbuild/prerender_entry.js @@ -0,0 +1,128 @@ +/** @import { SSRManifest } from '@sveltejs/kit' */ +/** @import { InternalServer, RemotePrerenderInternals } from 'types' */ +/** @import { SerialisedResponse } from '../../exports/vite/types.js' */ +import { get } from '__sveltekit/ipc'; +import { Server as KitServer } from '__SERVER__/index.js'; +import { get_hooks, set_building, set_prerendering } from '__SERVER__/internal.js'; +import { stringify_remote_arg } from '../../runtime/shared.js'; + +set_building(); +set_prerendering(); + +export class Server extends KitServer { + #manifest; + + /** @param {SSRManifest} manifest */ + constructor(manifest) { + super(manifest); + + this.#manifest = manifest; + + import.meta.hot?.on('sveltekit:prerender-assets', (data) => { + manifest.assets.add(data); + }); + } + + /** @type {InternalServer['init']} */ + async init(options) { + options.read = async (file) => { + const response = await get(`/read?${new URLSearchParams({ file })}`); + if (!response.ok) { + throw new Error( + `read(...) failed: could not fetch ${file} (${response.status} ${response.statusText})` + ); + } + return response.body; + }; + + return super.init(options); + } + + /** @type {InternalServer['respond']} */ + async respond(request, options) { + options.getClientAddress = () => { + throw new Error('Cannot read clientAddress during prerendering'); + }; + + options.prerendering = { + fallback: request.url.endsWith('/[fallback]'), + dependencies: new Map(), + remote_responses: import.meta.hot?.data.remote_responses + }; + + options.read = async (file) => { + const response = await get(`/prerender-read?${new URLSearchParams({ file })}`); + return Buffer.from(await response.arrayBuffer()); + }; + + const url = new URL(request.url); + + if (url.pathname === `/${this.#manifest.appPath}/prerender-functions`) { + const names = url.searchParams.getAll('name'); + const pathnames = await get_prerender_function_paths(this.#manifest, names); + return Response.json(pathnames); + } + + const response = await super.respond(request, options); + + /** @type {Map} */ + const dependencies = new Map(); + for (const [pathname, dependency] of options.prerendering.dependencies) { + dependencies.set(pathname, { + response: { + status: dependency.response.status, + statusText: dependency.response.statusText, + headers: Object.fromEntries(dependency.response.headers), + body: await dependency.response.arrayBuffer() + }, + body: dependency.body + }); + } + import.meta.hot?.send(`sveltekit:prerender-dependencies:${url.pathname}`, { + dependencies: Object.fromEntries(dependencies) + }); + + return response; + } +} + +/** + * @param {SSRManifest} manifest + * @param {string[]} names + * @returns {Promise} + */ +async function get_prerender_function_paths(manifest, names) { + /** @type {RemotePrerenderInternals[]} */ + const prerender_functions = []; + + for (const name of names) { + const module = await manifest._.remotes[name](); + + for (const fn of Object.values(module.default)) { + if (fn?.__?.type === 'prerender') { + prerender_functions.push(fn.__); + } + } + } + + const remote_prefix = `/${manifest.appPath}/remote/`; + + /** @type {string[]} */ + const pathnames = []; + + const transport = (await get_hooks()).transport ?? {}; + for (const internals of prerender_functions) { + if (internals.has_arg) { + for (const arg of (await internals.inputs?.()) ?? []) { + pathnames.push(remote_prefix + internals.id + '/' + stringify_remote_arg(arg, transport)); + } + } else { + pathnames.push(remote_prefix + internals.id); + } + } + return pathnames; +} + +if (import.meta.hot) { + import.meta.hot.data.remote_responses ??= new Map(); +} diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index d7ffe012d6bd..1ad4996e0282 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -2,14 +2,16 @@ import { lookup } from 'mrmime'; import fs from 'node:fs'; import path from 'node:path'; import { styleText } from 'node:util'; -import { posixify, resolve_entry } from '../../../utils/filesystem.js'; +import { resolve_entry } from '../../../utils/filesystem.js'; +import { posixify } from '../../../utils/os.js'; import { parse_route_id } from '../../../utils/routing.js'; -import { list_files, runtime_directory } from '../../utils.js'; +import { list_files } from '../../utils.js'; import { sort_routes } from './sort.js'; import { create_node_analyser, get_page_options } from '../../../exports/vite/static_analysis/index.js'; +import { runtime_directory } from '../../../runtime/utils.js'; /** * Generates the manifest data used for the client-side manifest and types generation. diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index 3b92b73dbc1b..9d945461d8a9 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -1,6 +1,6 @@ import path from 'node:path'; import { GENERATED_COMMENT } from '../../constants.js'; -import { posixify } from '../../utils/filesystem.js'; +import { posixify } from '../../utils/os.js'; import { write_if_changed } from './utils.js'; import { s } from '../../utils/misc.js'; import { get_route_segments } from '../../utils/routing.js'; diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index ebc1db336184..acfaed103c53 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -1,10 +1,11 @@ import path from 'node:path'; import { styleText } from 'node:util'; import { hash } from '../../utils/hash.js'; -import { posixify, resolve_entry } from '../../utils/filesystem.js'; +import { resolve_entry } from '../../utils/filesystem.js'; +import { posixify } from '../../utils/os.js'; import { s } from '../../utils/misc.js'; import { load_error_page, load_template } from '../config/index.js'; -import { runtime_directory } from '../utils.js'; +import { runtime_directory } from '../../runtime/utils.js'; import { write_if_changed } from './utils.js'; import { escape_html } from '../../utils/escape.js'; diff --git a/packages/kit/src/core/sync/write_tsconfig.js b/packages/kit/src/core/sync/write_tsconfig.js index 1845a962e2cd..eaef8e2ee55d 100644 --- a/packages/kit/src/core/sync/write_tsconfig.js +++ b/packages/kit/src/core/sync/write_tsconfig.js @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { styleText } from 'node:util'; -import { posixify } from '../../utils/filesystem.js'; +import { posixify } from '../../utils/os.js'; import { write_if_changed } from './utils.js'; /** diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index 3ecd36c23c7c..21fd68d79d52 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -1,8 +1,9 @@ import fs from 'node:fs'; import path from 'node:path'; import MagicString from 'magic-string'; -import { posixify, rimraf, walk } from '../../../utils/filesystem.js'; +import { rimraf, walk } from '../../../utils/filesystem.js'; import { compact } from '../../../utils/array.js'; +import { posixify } from '../../../utils/os.js'; import { ts } from '../ts.js'; const remove_relative_parent_traversals = (/** @type {string} */ path) => path.replace(/\.\.\//g, ''); diff --git a/packages/kit/src/core/utils.js b/packages/kit/src/core/utils.js index 6e63c3cb582b..991c6091e061 100644 --- a/packages/kit/src/core/utils.js +++ b/packages/kit/src/core/utils.js @@ -1,19 +1,10 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { styleText } from 'node:util'; -import { posixify, to_fs } from '../utils/filesystem.js'; +import { to_fs } from '../exports/vite/filesystem.js'; +import { runtime_directory } from '../runtime/utils.js'; import { noop } from '../utils/functions.js'; - -/** - * Resolved path of the `runtime` directory - * - * TODO Windows issue: - * Vite or sth else somehow sets the driver letter inconsistently to lower or upper case depending on the run environment. - * In playwright debug mode run through VS Code this a root-to-lowercase conversion is needed in order for the tests to run. - * If we do this conversion in other cases it has the opposite effect though and fails. - */ -export const runtime_directory = posixify(fileURLToPath(new URL('../runtime', import.meta.url))); +import { posixify } from '../utils/os.js'; /** * This allows us to import SvelteKit internals that aren't exposed via `pkg.exports` in a @@ -24,7 +15,7 @@ export const runtime_directory = posixify(fileURLToPath(new URL('../runtime', im */ export function get_runtime_base(root) { return runtime_directory.startsWith(root) - ? `/${path.relative(root, runtime_directory)}` + ? `/${posixify(path.relative(root, runtime_directory))}` : to_fs(runtime_directory); } diff --git a/packages/kit/src/exports/internal/index.js b/packages/kit/src/exports/internal/index.js index eb9589c368d4..68eb1b74a95a 100644 --- a/packages/kit/src/exports/internal/index.js +++ b/packages/kit/src/exports/internal/index.js @@ -88,3 +88,8 @@ export class ValidationError extends Error { } export { init_remote_functions } from './remote-functions.js'; + +// re-exporting this allows us to import it from generated modules under @sveltejs/kit/internal +// whereas importing devalue directly would error if the user doesn't have it in +// their package.json +export * as devalue from 'devalue'; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 6a5f7bb9de31..a4478aafe2e7 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -22,6 +22,7 @@ import { import { BuildData, SSRNodeLoader, SSRRoute, ValidatedConfig } from 'types'; import { SvelteConfig } from '@sveltejs/vite-plugin-svelte'; import { StandardSchemaV1 } from '@standard-schema/spec'; +import { PluginOption } from 'vite'; import { RouteId as AppRouteId, LayoutParams as AppLayoutParams, @@ -62,11 +63,14 @@ export interface Adapter { */ instrumentation?: () => boolean; }; - /** - * Creates an `Emulator`, which allows the adapter to influence the environment - * during dev, build and prerendering. - */ - emulate?: () => MaybePromise; + vite?: { + /** + * Add a Vite plugin here to replace the default Node SSR environment. + * The provided Vite plugins should configure the dev and preview servers + * @since 3.0.0 + */ + plugins?: PluginOption; + }; } export type LoadProperties | void> = input extends void @@ -103,6 +107,12 @@ type UnpackValidationError = ? undefined // needs to be undefined, because void will corrupt union type : T; +export interface ManifestGenerationOptions { + /** A relative path to the base directory of the server build output */ + relativePath: string; + routes?: RouteDefinition[]; +} + /** * This object is passed to the `adapt` function of adapters. * It contains various methods and properties that are useful for adapting the app. @@ -146,9 +156,8 @@ export interface Builder { /** * Generate a server-side manifest to initialise the SvelteKit [server](https://svelte.dev/docs/kit/@sveltejs-kit#Server) with. - * @param opts a relative path to the base directory of the app and optionally in which format (esm or cjs) the manifest should be generated */ - generateManifest: (opts: { relativePath: string; routes?: RouteDefinition[] }) => string; + generateManifest: (opts: ManifestGenerationOptions) => string; /** * Resolve a path to the `name` directory inside `outDir`, e.g. `/path/to/.svelte-kit/my-adapter`. @@ -309,21 +318,12 @@ 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 { + // TODO: remove this in 4.0 /** * Your [adapter](https://svelte.dev/docs/kit/adapters) is run when executing `vite build`. It determines how the output is converted for different platforms. * @default undefined + * @deprecated removed in 3.0.0. Adapters should now be passed to the `sveltekit` Vite plugin in `vite.config.js` */ adapter?: Adapter; /** @@ -909,6 +909,15 @@ export interface KitConfig { }; } +export interface KitViteConfig { + /** + * Your [adapter](https://svelte.dev/docs/kit/adapters) is run when executing `vite build`. It determines how the output is converted for different platforms. + * @since 3.0.0 + * @default undefined + */ + adapter?: Adapter; +} + /** * The [`handle`](https://svelte.dev/docs/kit/hooks#Server-hooks-handle) hook runs every time the SvelteKit server receives a [request](https://svelte.dev/docs/kit/web-standards#Fetch-APIs-Request) and * determines the [response](https://svelte.dev/docs/kit/web-standards#Fetch-APIs-Response). @@ -1645,19 +1654,25 @@ export interface ServerInitOptions { read?: (file: string) => MaybePromise; } +/** + * Required to instantiate `Server` with project specific information + */ 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; + base: string; /** Static files from `kit.config.files.assets` and the service worker (if any). */ assets: Set; mimeTypes: Record; - /** private fields */ + /** @internal private fields */ _: { client: NonNullable; nodes: SSRNodeLoader[]; /** hashed filename -> import to that file */ - remotes: Record Promise>; + remotes: Record Promise<{ default: Record }>>; routes: SSRRoute[]; prerendered_routes: Set; matchers: () => Promise>; diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index 65ae7914a291..5e4bb422a87a 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -1,3 +1,6 @@ +/** @import { AssetDependencies, ManifestData, RecursiveRequired, SSRNode, ValidatedConfig, ValidatedKitConfig } from 'types' */ +/** @import { Manifest, Rolldown } from 'vite' */ + import fs from 'node:fs'; import { mkdirp } from '../../../utils/filesystem.js'; import { @@ -14,27 +17,55 @@ import { fix_css_urls } from '../../../utils/css.js'; import { escape_for_interpolation } from '../../../utils/escape.js'; /** - * @param {string} out - * @param {import('types').ValidatedKitConfig} kit - * @param {import('types').ManifestData} manifest_data - * @param {import('vite').Manifest} server_manifest - * @param {import('vite').Manifest | null} client_manifest - * @param {string | null} assets_path - * @param {import('vite').Rolldown.RolldownOutput['output'] | null} client_chunks - * @param {import('types').RecursiveRequired} output_config - * @param {string} root + * @overload partial build to analyse the server nodes + * @param {object} opts + * @param {string} opts.out + * @param {ManifestData} opts.manifest_data + * @param {Manifest} opts.server_manifest + * @param {string} opts.root + * @returns {void} + */ +/** + * @overload final build + * @param {object} opts + * @param {string} opts.out + * @param {ManifestData} opts.manifest_data + * @param {Manifest} opts.server_manifest + * @param {Manifest} opts.client_manifest + * @param {ValidatedKitConfig['paths']} opts.paths + * @param {number} opts.inline_style_threshold + * @param {string} opts.assets_path + * @param {Rolldown.RolldownOutput['output']} opts.client_chunks + * @param {RecursiveRequired} opts.output_config + * @param {string} opts.root + * @returns {void} + */ +/** + * @param {object} opts + * @param {string} opts.out + * @param {ManifestData} opts.manifest_data + * @param {Manifest} opts.server_manifest + * @param {Manifest} [opts.client_manifest] + * @param {ValidatedKitConfig['paths']} [opts.paths] + * @param {number} [opts.inline_style_threshold] + * @param {string} [opts.assets_path] path to `_app/immutable/assets` + * @param {Rolldown.RolldownOutput['output']} [opts.client_chunks] + * @param {RecursiveRequired} [opts.output_config] + * @param {string} opts.root + * @returns {void} */ -export function build_server_nodes( +export function build_server_nodes({ out, - kit, manifest_data, server_manifest, client_manifest, + paths, + inline_style_threshold, assets_path, client_chunks, output_config, root -) { +}) { mkdirp(`${out}/server/nodes`); mkdirp(`${out}/server/stylesheets`); @@ -51,14 +82,19 @@ export function build_server_nodes( */ let prepare_css_for_inlining = (css) => s(css); - if (client_chunks && kit.inlineStyleThreshold > 0 && output_config.bundleStrategy === 'split') { + if ( + client_chunks && + inline_style_threshold && + inline_style_threshold > 0 && + output_config?.bundleStrategy === 'split' + ) { for (const chunk of client_chunks) { if (chunk.type !== 'asset' || !chunk.fileName.endsWith('.css')) { continue; } const source = chunk.source.toString(); - if (source.length < kit.inlineStyleThreshold) { + if (source.length < inline_style_threshold) { stylesheets_to_inline.set(chunk.fileName, source); } } @@ -67,7 +103,7 @@ export function build_server_nodes( // relative path so that they are correct when inlined into the document. // Although `paths.assets` is static, we need to pass in a fake path // `/_svelte_kit_assets` at runtime when running `vite preview` - if (kit.paths.assets || kit.paths.relative) { + if (paths?.assets || paths?.relative) { const static_assets = new Set( manifest_data.assets.map((asset) => decodeURIComponent(asset.file)) ); @@ -115,7 +151,7 @@ export function build_server_nodes( const imports = []; // String representation of - /** @type {import('types').SSRNode} */ + /** @type {SSRNode} */ /** @type {string[]} */ const exports = [`export const index = ${i};`]; @@ -164,22 +200,22 @@ export function build_server_nodes( if ( client_manifest && (node.universal || node.component) && - output_config.bundleStrategy === 'split' + output_config?.bundleStrategy === 'split' ) { - const entry_path = `${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`; + const entry_path = `${normalizePath(`${out}/..`)}/generated/client-optimized/nodes/${i}.js`; const entry = find_deps(client_manifest, entry_path, true, root); // Eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC. // However, if it is not used during SSR (not present in the server manifest), // then it can be lazily loaded in the browser. - /** @type {import('types').AssetDependencies | undefined} */ + /** @type {AssetDependencies | undefined} */ let component; if (node.component) { component = find_deps(server_manifest, node.component, true, root); } - /** @type {import('types').AssetDependencies | undefined} */ + /** @type {AssetDependencies | undefined} */ let universal; if (node.universal) { universal = find_deps(server_manifest, node.universal, true, root); diff --git a/packages/kit/src/exports/vite/build/remote.js b/packages/kit/src/exports/vite/build/remote.js index ddd91c6a8927..65eac5f1762c 100644 --- a/packages/kit/src/exports/vite/build/remote.js +++ b/packages/kit/src/exports/vite/build/remote.js @@ -5,7 +5,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { Parser } from 'acorn'; import MagicString from 'magic-string'; -import { posixify } from '../../../utils/filesystem.js'; +import { posixify } from '../../../utils/os.js'; /** * @param {typeof import('vite')} vite diff --git a/packages/kit/src/exports/vite/build/vite_server.js b/packages/kit/src/exports/vite/build/vite_server.js new file mode 100644 index 000000000000..05b7da21b0be --- /dev/null +++ b/packages/kit/src/exports/vite/build/vite_server.js @@ -0,0 +1,410 @@ +/** @import { Adapter } from '@sveltejs/kit' */ +/** @import { ValidatedConfig } from 'types' */ +/** @import { Connect, PluginOption, ViteDevServer } from 'vite' */ +/** @import { ModuleRunner } from 'vite/module-runner' */ +import fs from 'node:fs'; +import path, { basename } from 'node:path'; +import { exactRegex } from 'rolldown/filter'; +import sirv from 'sirv'; +import { + createFetchableDevEnvironment, + createServer, + createServerHotChannel, + createServerModuleRunner, + isFetchableDevEnvironment, + resolveConfig +} from 'vite'; +import { getRequest, setResponse } from '@sveltejs/kit/node'; +import { sveltekit_env, sveltekit_ipc } from '../module_ids.js'; +import { dedent } from '../../../core/sync/utils.js'; +import { + check_feature, + create_app_dir_matcher, + has_correct_case, + invalidate_module, + remove_static_middlewares +} from '../dev/index.js'; +import { s } from '../../../utils/misc.js'; +import { get_env } from '../utils.js'; +import { SVELTE_KIT_ASSETS } from '../../../constants.js'; + +/** + * Spins up a Vite dev server along with the build output so that we can run + * analysis and prerendering in the environment itself. This helps us avoid + * runtime errors when the user imports non-Node runtime APIs such as `cloudflare:workers`. + * We achieve this by using Vite's `resolveId` hook to intercept module resolution + * and provide a `Server` class that runs our custom instructions. + * @param {object} opts + * @param {ValidatedConfig} opts.svelte_config + * @param {string} opts.out + * @param {string} opts.manifest_path + * @param {string} opts.server_path path to the module with our custom Server export + * @param {PluginOption} [opts.vite_plugins] additional plugins to customise the Vite behaviour + * @returns {Promise} + */ +export async function create_build_server({ + svelte_config, + out, + manifest_path, + server_path, + vite_plugins +}) { + const vite_config = await resolveConfig({}, 'build'); + /** @type {Adapter | undefined} */ + const adapter = vite_config.plugins.find( + (plugin) => plugin.name === 'vite-plugin-sveltekit-adapter' + )?.api?.adapter; + + /** @type {number | undefined} */ + let port; + + const app_path = `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}`; + + /** + * Allows us to perform Node operations from a non-Node environment by sending + * a request to the Vite dev server. We can then configure a middleware to + * intercept, operate, and respond. + * + * We can't achieve the same with import.meta.hot and Promise.withResolver() + * because Cloudflare's workerd doesn't like it when we await a promise resolved + * from import.meta.hot + * @type {PluginOption} + */ + const plugin_ipc = { + name: 'vite-plugin-sveltekit-compile:ipc', + configureServer(vite) { + return () => { + vite.middlewares.use((_req, _res, next) => { + // ensure the server port is up-to-date + const address = vite.httpServer?.address(); + const current_port = + typeof address === 'string' ? Number(address.split(':').at(-1)) : address?.port; + if (current_port && current_port !== port) { + port = current_port; + vite.environments.ssr.hot.send('sveltekit:port', port); + invalidate_module(vite, sveltekit_ipc); + } + + next(); + }); + }; + }, + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + resolveId: { + filter: { + id: exactRegex('__sveltekit/ipc') + }, + handler() { + return '\0virtual:__sveltekit/ipc'; + } + }, + load: { + filter: { + id: exactRegex(sveltekit_ipc) + }, + handler() { + 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(\`http://localhost:\${port}${app_path}\${pathname}\`); + } + + let port${port ? ` = ${port}` : ''}; + import.meta.hot?.on('sveltekit:port', (update) => { port = update }); + `; + } + } + }; + + /** @type {{ public: Record; private: Record }} */ + let env; + + /** @type {Connect.ServerStackItem | undefined} */ + let serve_static_middleware; + + /** @type {PluginOption} */ + const plugin_server = { + name: 'vite-plugin-sveltekit-compile:build-entry', + config(_, vite_config_env) { + env = get_env(svelte_config.kit.env, vite_config_env.mode); + + return { + appType: 'custom', + cacheDir: `node_modules/.vite-${basename(server_path, '.js')}`, + environments: { + ssr: { + build: { + outDir: `${out}/server` + } + } + }, + publicDir: `${out}/client`, + resolve: { + alias: [ + { + find: '__SERVER__', + replacement: `${out}/server` + } + ] + }, + server: { + watch: { + ignored: out + } + } + }; + }, + configureServer(vite) { + const assets = svelte_config.kit.paths.assets + ? SVELTE_KIT_ASSETS + : svelte_config.kit.paths.base; + + const asset_server = sirv(`${out}/client`, { + dev: true, + etag: true, + maxAge: 0, + extensions: [], + setHeaders: (res) => { + res.setHeader('access-control-allow-origin', '*'); + } + }); + + vite.middlewares.use((req, res, next) => { + const base = `${vite.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + const decoded = decodeURI(new URL(base + req.url).pathname); + + if (decoded.startsWith(assets)) { + const pathname = decoded.slice(assets.length); + const file = svelte_config.kit.files.assets + pathname; + + if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) { + if (has_correct_case(file, svelte_config.kit.files.assets)) { + req.url = encodeURI(pathname); // don't need query/hash + asset_server(req, res); + return; + } + } + } + + next(); + }); + + const read_pathname = create_app_dir_matcher( + svelte_config.kit.paths.base, + svelte_config.kit.appDir, + '/read' + ); + + const check_feature_pathname = create_app_dir_matcher( + svelte_config.kit.paths.base, + svelte_config.kit.appDir, + '/check-feature' + ); + + return () => { + serve_static_middleware = vite.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); + + vite.middlewares.use((req, res, next) => { + // Vite's base middleware strips out the base path. Restore it + req.url = req.originalUrl; + + const base = `${vite.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + const url = new URL(base + req.url); + const decoded = decodeURI(url.pathname); + + 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, adapter); + + res.writeHead(200); + res.end(result?.message); + return; + } + + if (decoded.match(read_pathname)) { + const file = url.searchParams.get('file'); + if (!file) { + res.writeHead(400); + res.end('Missing file query argument'); + return; + } + + const readable_stream = fs.createReadStream( + `${svelte_config.kit.outDir}/output/server/${file}` + ); + + res.writeHead(200); + readable_stream.pipe(res); + return; + } + + next(); + }); + }; + }, + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + resolveId: { + filter: { + id: [ + exactRegex('sveltekit:server-manifest'), + exactRegex('sveltekit:server'), + exactRegex('sveltekit:env') + ] + }, + handler(id) { + if (id === 'sveltekit:server-manifest') { + return manifest_path; + } + + // substitute the Server class with our script instead + if (id === 'sveltekit:server') { + return server_path; + } + + if (id === 'sveltekit:env') { + return sveltekit_env; + } + } + }, + load: { + filter: { + id: exactRegex(sveltekit_env) + }, + handler() { + return `export const env = ${s({ ...env.private, ...env.public })};`; + } + } + }; + + /** @type {ModuleRunner} */ + let runner; + + /** @type {string | undefined} */ + let remote_address; + + /** @type {PluginOption} */ + const plugin_node_environment = { + name: 'vite-plugin-sveltekit-compile:node-environment', + config() { + return { + environments: { + ssr: { + dev: { + createEnvironment(name, config) { + return createFetchableDevEnvironment(name, config, { + hot: true, + transport: 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; + } + } + }); + } + } + } + } + }; + }, + async configureServer(vite) { + if (runner) await runner.close(); + runner = createServerModuleRunner(vite.environments.ssr); + + return () => { + vite.middlewares.use(async (req, res, next) => { + remote_address = req.socket.remoteAddress; + + // Vite's base middleware strips out the base path. Restore it + req.url = req.originalUrl; + + const base = `${vite.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + // fallback to our own fetch handler if the adapter doesn't provide one + if (!isFetchableDevEnvironment(vite.environments.ssr)) { + throw new Error( + 'The Vite configured dev SSR environment must be a FetchableDevEnvironment' + ); + } + + const request = await getRequest({ + base, + request: req + }); + const response = await vite.environments.ssr.dispatchFetch(request); + + if (response.status === 404) { + // @ts-expect-error + serve_static_middleware?.handle(req, res, () => { + void setResponse(res, response); + }); + } else { + void setResponse(res, response); + } + + next(); + }); + }; + }, + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + resolveId: { + filter: { + id: exactRegex('__sveltekit/dev-server-entry.js') + }, + handler() { + return path.join(import.meta.dirname, '../dev/ssr_entry.js'); + } + } + }; + + /** @type {PluginOption} */ + const plugins = [ + vite_plugins, + plugin_ipc, + plugin_server, + adapter?.vite?.plugins ?? plugin_node_environment + ].filter(Boolean); + + return await createServer({ + configFile: false, + command: 'serve', + plugins + }); +} diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 23cbf21b3c58..9d2c78cc6114 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -1,24 +1,20 @@ -/** @import { RequestEvent } from '@sveltejs/kit' */ -/** @import { PrerenderOption, UniversalNode } from 'types' */ 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 { isCSSRequest, isFetchableDevEnvironment } from 'vite'; +import { getRequest, setResponse } from '../../../exports/node/index.js'; import { coalesce_to_error } from '../../../utils/error.js'; -import { from_fs, posixify, resolve_entry, to_fs } from '../../../utils/filesystem.js'; +import { resolve_entry } from '../../../utils/filesystem.js'; +import { posixify } from '../../../utils/os.js'; import { load_error_page } from '../../../core/config/index.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 { compact } from '../../../utils/array.js'; import { is_chrome_devtools_request, 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'; +import { to_fs } from '../filesystem.js'; // vite-specifc queries that we should skip handling for css urls const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/; @@ -27,100 +23,34 @@ 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 {string} root The project root directory - * @return {Promise void>>} + * @param {import('types').DevEnvironment} dev_environment + * @param {import('@sveltejs/kit').Adapter | undefined} adapter + * @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(vite, vite_config, svelte_config, root, dev_environment, adapter) { sync.init(svelte_config, vite_config.mode, root); /** @type {import('types').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 }); - } - - 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.posix.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 }; - } - function update_manifest() { try { ({ manifest_data } = sync.create(svelte_config, root)); + dev_environment.manifest_data = manifest_data; if (manifest_error) { manifest_error = null; - vite.ws.send({ type: 'full-reload' }); + vite.hot.send({ type: 'full-reload' }); } } catch (error) { manifest_error = /** @type {Error} */ (error); console.error(styleText(['bold', 'red'], manifest_error.message)); - vite.ws.send({ + vite.hot.send({ type: 'error', err: { message: manifest_error.message ?? 'Invalid routes', @@ -131,195 +61,7 @@ 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 () => { - /** @type {Record} */ - const matchers = {}; - - for (const key in manifest_data.matchers) { - const file = manifest_data.matchers[key]; - const url = path.resolve(root, file); - const module = await vite.ssrLoadModule(url, { fixStacktrace: true }); - - if (module.match) { - matchers[key] = module.match; - } else { - throw new Error(`${file} does not export a \`match\` function`); - } - } - - return matchers; - } - } - }; - } - - /** @param {Error} error */ - function fix_stack_trace(error) { - 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 - } - return error.stack; + invalidate_module(vite, sveltekit_manifest_data); } update_manifest(); @@ -366,6 +108,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 || restarting || !/\+(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 + ); + vite.environments.ssr.hot.send('sveltekit:manifest-data', { + nodes_page_options, + endpoints_page_options + }); + invalidate_module(vite, sveltekit_manifest_data); }); const { appTemplate, errorTemplate, serviceWorker, hooks } = svelte_config.kit.files; @@ -404,6 +156,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, @@ -437,10 +190,21 @@ 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' + ); return () => { + // ensure it has the correct server port + invalidate_module(vite, sveltekit_ipc); + const serve_static_middleware = vite.middlewares.stack.find( (middleware) => /** @type {Function} */ (middleware.handle).name === 'viteServeStaticMiddleware' @@ -450,7 +214,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { // serving routes with those names. See https://github.com/vitejs/vite/issues/7363 remove_static_middlewares(vite.middlewares); - vite.middlewares.use(async (req, res) => { + vite.middlewares.use(async (req, res, next) => { // Vite's base middleware strips out the base path. Restore it const original_url = req.url; req.url = req.originalUrl; @@ -459,7 +223,9 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) { 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)) ); @@ -483,6 +249,34 @@ 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(vite, urls); + 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, 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); @@ -499,41 +293,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)}/shared-server.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 = await getRequest({ - base, - request: req - }); - if (manifest_error) { console.error(styleText(['bold', 'red'], manifest_error.message)); @@ -556,37 +315,36 @@ 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: (event, config, prerender) => { - async_local_storage.enterWith({ event, config, prerender }); - }, - emulator - }); - - if (rendered.status === 404) { - // @ts-expect-error - serve_static_middleware.handle(req, res, () => { - void setResponse(res, rendered); + // fallback to our own fetch handler if the adapter doesn't provide one + if (!adapter?.vite?.plugins) { + if (!isFetchableDevEnvironment(vite.environments.ssr)) { + throw new Error( + 'The Vite configured dev SSR environment must be a FetchableDevEnvironment' + ); + } + + const request = await getRequest({ + base, + request: req }); - } else { - void setResponse(res, rendered); + const response = await vite.environments.ssr.dispatchFetch(request); + + if (response.status === 404) { + // @ts-expect-error + serve_static_middleware.handle(req, res, () => { + void setResponse(res, response); + }); + } else { + void setResponse(res, response); + } + return; } + + next(); } catch (e) { const error = coalesce_to_error(e); res.statusCode = 500; - res.end(fix_stack_trace(error)); + res.end(error.stack); } }); }; @@ -595,7 +353,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 @@ -626,23 +384,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))); @@ -651,16 +406,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 @@ -670,7 +415,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); @@ -681,3 +426,85 @@ function has_correct_case(file, assets) { return false; } + +/** + * @param {import('vite').ViteDevServer} vite + * @param {string} id + * @returns {void} + */ +export function invalidate_module(vite, id) { + for (const environment in vite.environments) { + const module = vite.environments[environment].moduleGraph.getModuleById(id); + if (module) { + vite.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 {import('vite').ViteDevServer} vite + * @param {string[]} urls + * @returns {Promise>} + */ +async function get_inline_css(vite, urls) { + /** @type {Set} */ + const deps = new Set(); + + for (const url of urls) { + const module_node = await vite.environments.ssr.moduleGraph.getModuleByUrl(url); + if (!module_node) throw new Error(`Could not find node for ${url}`); + await find_deps(vite, module_node, deps); + } + + /** @type {Map} */ + const styles = new Map(); + + for (const dep of deps) { + if (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 {import('@sveltejs/kit').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.` + }; + } + } + } +} 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..b72347b43350 --- /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 '../filesystem.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..a2ea05dd42da --- /dev/null +++ b/packages/kit/src/exports/vite/dev/ssr_manifest.js @@ -0,0 +1,203 @@ +/** @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 { to_fs } from '../filesystem.js'; +import { compact } from '../../../utils/array.js'; +import { join } from '../../../utils/path.js'; +import { runtime_directory } from '../../../runtime/utils.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: to_fs(`${runtime_directory}/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 () => { + const importing_matchers = Object.entries(manifest_data.matchers).map( + async ([name, file]) => { + const url = join(__SVELTEKIT_ROOT__, file); + const { module } = await resolve(url); + if (!module.match) { + throw new Error(`${file} does not export a \`match\` function`); + } + return [name, module.match]; + } + ); + return Object.fromEntries(await Promise.all(importing_matchers)); + } + } +}; + +/** @param {string} url */ +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 */ +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/filesystem.js b/packages/kit/src/exports/vite/filesystem.js new file mode 100644 index 000000000000..4214cc04f881 --- /dev/null +++ b/packages/kit/src/exports/vite/filesystem.js @@ -0,0 +1,29 @@ +// this file needs to be runtime agnostic and avoid importing from `node:*` since +// it may not be available in edge environments + +import { posixify } from '../../utils/os.js'; + +/** + * Prepend given path with `/@fs` prefix + * @param {string} str + */ +export function to_fs(str) { + str = posixify(str); + return `/@fs${ + // Windows/Linux separation - Windows starts with a drive letter, we need a / in front there + str.startsWith('/') ? '' : '/' + }${str}`; +} + +/** + * Removes `/@fs` prefix from given path and posixifies it + * @param {string} str + */ +export function from_fs(str) { + str = posixify(str); + if (!str.startsWith('/@fs')) return str; + + str = str.slice(4); + // Windows/Linux separation - Windows starts with a drive letter, we need to strip the additional / here + return str[2] === ':' && /[A-Z]/.test(str[1]) ? str.slice(1) : str; +} diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index dd55018dc9b5..fe72bd8d464c 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1,22 +1,32 @@ +/** @import { Adapter, KitViteConfig } from '@sveltejs/kit' */ /** @import { Options } from '@sveltejs/vite-plugin-svelte' */ /** @import { PreprocessorGroup } from 'svelte/compiler' */ -/** @import { ConfigEnv, Plugin, ResolvedConfig, UserConfig, ViteDevServer } from 'vite' */ +/** @import { ValidatedConfig, ValidatedKitConfig } from 'types' */ +/** @import { ConfigEnv, Plugin, PluginOption, ResolvedConfig, UserConfig, Manifest, EnvironmentOptions, Rolldown } from 'vite' */ +/** @import { ModuleRunner } from 'vite/module-runner' */ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { styleText } from 'node:util'; +import * as devalue from 'devalue'; import { exactRegex, prefixRegex } from 'rolldown/filter'; +import { + buildErrorMessage, + createFetchableDevEnvironment, + createServerHotChannel, + createServerModuleRunner +} from 'vite'; -import { copy, mkdirp, posixify, read, resolve_entry, rimraf } from '../../utils/filesystem.js'; +import { copy, mkdirp, read, resolve_entry, rimraf } from '../../utils/filesystem.js'; import { create_static_module, create_dynamic_module } from '../../core/env.js'; import * as sync from '../../core/sync/sync.js'; import { create_assets } from '../../core/sync/create_manifest_data/index.js'; -import { runtime_directory, logger } from '../../core/utils.js'; +import { logger, get_mime_lookup } from '../../core/utils.js'; import { generate_manifest } from '../../core/generate_manifest/index.js'; import { build_server_nodes } from './build/build_server.js'; import { assets_base, 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, @@ -31,7 +41,7 @@ import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; import { hash } from '../../utils/hash.js'; -import { dedent } from '../../core/sync/utils.js'; +import { dedent, write_if_changed } from '../../core/sync/utils.js'; import { app_server, env_dynamic_private, @@ -39,20 +49,33 @@ import { env_static_private, env_static_public, service_worker, + sveltekit_remotes, sveltekit_environment, - sveltekit_server + sveltekit_server, + sveltekit_traced, + sveltekit_manifest_data, + sveltekit_env, + sveltekit_ipc } from './module_ids.js'; +import { to_fs } from './filesystem.js'; import { import_peer } from '../../utils/import.js'; import { compact } from '../../utils/array.js'; +import { posixify } from '../../utils/os.js'; import { should_ignore, has_children } from './static_analysis/utils.js'; import { load_config } from '../../core/config/index.js'; import { treeshake_prerendered_remotes } from './build/remote.js'; +import { runtime_directory } from '../../runtime/utils.js'; +import { SVELTE_KIT_ASSETS } from '../../constants.js'; +import options from './options.js'; const cwd = process.cwd(); /** @type {string} */ let root; +/** @type {import('types').DevEnvironment | null} */ +let dev_environment = null; + /** @type {import('./types.js').EnforcedConfig} */ const enforced_config = { appType: true, @@ -140,12 +163,16 @@ let vite_plugin_svelte; /** * Returns the SvelteKit Vite plugins. - * @returns {Promise} + * @param {KitViteConfig} [config] + * @returns {Promise} */ -export async function sveltekit() { +export async function sveltekit(config) { + /** @type {KitViteConfig} */ + const validated = options(config, 'options'); + // the config options will be set only after the Vite `config` hook runs // because we need to find `svelte.config.js` relative to `vite.config.root` - const svelte_config = /** @type {import('types').ValidatedConfig} */ ({}); + const svelte_config = /** @type {ValidatedConfig} */ ({}); /** @type {Options} */ const vite_plugin_svelte_options = { @@ -158,12 +185,12 @@ export async function sveltekit() { return [ plugin_svelte_config({ vite_plugin_svelte_options, svelte_config }), - ...vite_plugin_svelte.svelte(vite_plugin_svelte_options), - ...kit({ svelte_config }) + vite_plugin_svelte.svelte(vite_plugin_svelte_options), + kit({ svelte_config, adapter: validated.adapter }) ]; } -/** @param {import('vite').UserConfig | import('vite').ResolvedConfig} vite_config */ +/** @param {UserConfig | ResolvedConfig} vite_config */ function resolve_root(vite_config) { return posixify(vite_config.root ? path.resolve(vite_config.root) : cwd); } @@ -173,9 +200,9 @@ function resolve_root(vite_config) { * of our other plugins try to access the config objects * @param {{ * vite_plugin_svelte_options: import('@sveltejs/vite-plugin-svelte').Options; - * svelte_config: import('types').ValidatedConfig; + * svelte_config: ValidatedConfig; * }} options - * @return {import('vite').Plugin} + * @return {Plugin} */ function plugin_svelte_config({ vite_plugin_svelte_options, svelte_config }) { return { @@ -218,6 +245,16 @@ function plugin_svelte_config({ vite_plugin_svelte_options, svelte_config }) { }; } +/** + * @param {unknown} value + * @returns {string | undefined} + */ +function revive_functions(value) { + if (value instanceof Function) { + return value.toString(); + } +} + /** * Returns the SvelteKit Vite plugin. Vite executes Rolldown hooks as well as some of its own. * Background reading is available at: @@ -228,14 +265,16 @@ function plugin_svelte_config({ vite_plugin_svelte_options, svelte_config }) { * - https://rolldown.rs/apis/plugin-api#build-hooks * - https://rolldown.rs/apis/plugin-api#output-generation-hooks * - * @param {{ svelte_config: import('types').ValidatedConfig }} options - * @return {import('vite').Plugin[]} + * @param {object} opts + * @param {ValidatedConfig} opts.svelte_config options are only resolved after the Vite `config` hook runs + * @param {Adapter | undefined} opts.adapter + * @return {PluginOption[]} */ -function kit({ svelte_config }) { +function kit({ svelte_config, adapter }) { /** @type {typeof import('vite')} */ let vite; - /** @type {import('types').ValidatedKitConfig} */ + /** @type {ValidatedKitConfig} */ let kit; /** @type {string} */ let out; @@ -256,7 +295,7 @@ function kit({ svelte_config }) { let env; /** @type {import('types').ManifestData} */ - let manifest_data; + let build_manifest_data; /** @type {import('types').ServerMetadata | undefined} only set at build time once analysis is finished */ let build_metadata = undefined; @@ -269,12 +308,16 @@ function kit({ svelte_config }) { /** @type {import('node:path').ParsedPath} */ let parsed_service_worker; + /** @type {string | null} */ + let server_instrumentation_file; + /** @type {string} */ let normalized_cwd; /** @type {string} */ let normalized_lib; /** @type {string} */ let normalized_node_modules; + /** * A map showing which features (such as `$app/server:read`) are defined * in which chunks, so that we can later determine which routes use which features @@ -313,6 +356,9 @@ function kit({ svelte_config }) { service_worker_entry_file = resolve_entry(kit.files.serviceWorker); parsed_service_worker = path.parse(kit.files.serviceWorker); + server_instrumentation_file = resolve_entry( + path.join(kit.files.src, 'instrumentation.server') + ); vite = await import_peer('vite', root); @@ -362,11 +408,10 @@ function kit({ svelte_config }) { preview: { cors: { preflightContinue: true } }, + // By default, only client environments inherit the top-level `optimizeDeps` + // but we manually pass it down in adapters that use `optimizeDeps` for "full-bundle mode" 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 @@ -375,7 +420,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: [ @@ -417,6 +464,7 @@ function kit({ svelte_config }) { const define = { __SVELTEKIT_APP_DIR__: s(kit.appDir), + __SVELTEKIT_OUT_DIR__: s(kit.outDir), __SVELTEKIT_EMBEDDED__: s(kit.embedded), __SVELTEKIT_FORK_PRELOADS__: s(kit.experimental.forkPreloads), __SVELTEKIT_PATHS_ASSETS__: s(kit.paths.assets), @@ -425,32 +473,30 @@ function kit({ svelte_config }) { __SVELTEKIT_CLIENT_ROUTING__: s(kit.router.resolution === 'client'), __SVELTEKIT_HASH_ROUTING__: s(kit.router.type === 'hash'), __SVELTEKIT_SERVER_TRACING_ENABLED__: s(kit.experimental.tracing.server), - __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__: s(kit.experimental.handleRenderingErrors) + __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__: s(kit.experimental.handleRenderingErrors), + __SVELTEKIT_ROOT__: s(root) }; if (is_build) { + if (!new_config.build) new_config.build = {}; + new_config.define = { ...define, - __SVELTEKIT_ADAPTER_NAME__: s(kit.adapter?.name), + __SVELTEKIT_ADAPTER_NAME__: s(adapter?.name), __SVELTEKIT_APP_VERSION_FILE__: s(`${kit.appDir}/version.json`), __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: s(kit.version.pollInterval) }; - manifest_data = sync.all(svelte_config, config_env.mode, root).manifest_data; + build_manifest_data = sync.all(svelte_config, config_env.mode, root).manifest_data; } else { new_config.define = { ...define, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0', __SVELTEKIT_PAYLOAD__: 'globalThis.__sveltekit_dev', __SVELTEKIT_HAS_SERVER_LOAD__: 'true', - __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true' + __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true', + __SVELTEKIT_FILES_ASSETS__: s(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' - ]; } warn_overridden_config(config, new_config); @@ -467,6 +513,321 @@ 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(config) { + /** @type {UserConfig} */ + const new_config = { + environments: { + ssr: { + dev: { + createEnvironment(name, config) { + return createFetchableDevEnvironment(name, config, { + hot: true, + transport: 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(vite) { + if (runner) await runner.close(); + runner = createServerModuleRunner(vite.environments.ssr); + + return () => { + vite.middlewares.use((req, _res, next) => { + remote_address = req.socket.remoteAddress; + next(); + }); + }; + }, + resolveId: { + filter: { + id: exactRegex('__sveltekit/dev-server-entry.js') + }, + handler() { + return server_instrumentation_file + ? sveltekit_traced + : 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', + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + configureServer() { + server_assets = new Map(); + }, + load: { + order: 'pre', + handler(id) { + if (!dev_environment) return; + + const { searchParams, search } = new URL(id, `file://`); + const pathname = id.replace(search, ''); + + if ( + (searchParams.has('url') || vite_config.assetsInclude(pathname)) && + fs.existsSync(pathname) + ) { + const filepath = pathname.startsWith(root) + ? posixify(path.relative(root, pathname)) + : to_fs(pathname); + // it should be a typed array for devalue to serialise it + const data = new Uint8Array(fs.readFileSync(pathname)); + const size = data.byteLength; + + // update it immediately + dev_environment.vite.environments.ssr.hot.send('sveltekit:server-assets', { + filepath, + size, + data: devalue.stringify(data) + }); + + // persist changes in case of server reload + server_assets.set(filepath, { size, data }); + invalidate_module(dev_environment.vite, '__sveltekit/server-assets'); + } + } + } + }; + + /** @type {number | undefined} */ + let port; + + /** @type {Plugin} */ + const plugin_dev_ssr = { + name: 'vite-plugin-sveltekit-dev-ssr', + apply: 'serve', + configureServer(vite) { + return () => { + vite.middlewares.use((_req, _res, next) => { + // ensure the server port is up-to-date + const address = vite.httpServer?.address(); + const current_port = + typeof address === 'string' ? Number(address.split(':').at(-1)) : address?.port; + if (current_port && current_port !== port) { + port = current_port; + vite.environments.ssr.hot.send('sveltekit:port', port); + invalidate_module(vite, 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 sveltekit_env; + } + + if (id === '__sveltekit/server-assets') { + /** @type {Array<[string, { size: number; data: Uint8Array }]>} */ + const entries = []; + + for (const asset of server_assets) { + entries.push(asset); + } + + const content = dedent` + import { devalue } from '@sveltejs/kit/internal'; + + 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); + }); + `; + const filepath = `${kit.outDir}/generated/server/server-assets.js`; + write_if_changed(filepath, content); + return filepath; + } + } + }, + load: { + filter: { + id: [ + exactRegex(sveltekit_env), + exactRegex(sveltekit_ipc), + exactRegex(sveltekit_remotes), + exactRegex(sveltekit_manifest_data), + exactRegex(sveltekit_traced) + ] + }, + handler(id) { + switch (id) { + case sveltekit_env: { + return `export const env = ${s({ ...env.private, ...env.public })};`; + } + + case sveltekit_ipc: { + if (!dev_environment) { + throw new Error('dev_environment was not initialised. But this should never happen'); + } + + const address = dev_environment.vite.httpServer?.address(); + const port = + typeof address === 'string' ? Number(address.split(':').at(-1)) : address?.port; + + const app_path = `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}`; + + 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(\`http://localhost:\${port}${app_path}\${pathname}\`); + } + + let port${port ? ` = ${port}` : ''}; + import.meta.hot?.on('sveltekit:port', (update) => { port = update }); + `; + } + + case sveltekit_remotes: { + return dedent` + export const remotes = ${s(remotes)}; + + import.meta.hot?.on('sveltekit:remotes', (data) => { + remotes.push(data); + }); + `; + } + + case sveltekit_manifest_data: { + if (!dev_environment) { + throw new Error('dev_environment was not initialised. But this should never happen'); + } + + const { manifest_data } = dev_environment; + + const assets = svelte_config.kit.paths.assets + ? SVELTE_KIT_ASSETS + : svelte_config.kit.paths.base; + + return dedent` + import { set_assets } from '__SERVER__/internal.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, revive_functions)}, + routes: ${devalue.uneval(manifest_data.routes)}, + matchers: ${s(manifest_data.matchers)} + }; + + 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_file) { + throw new Error('Server instrumentation file not found. This should never happen'); + } + + return dedent` + import '${posixify(server_instrumentation_file)}'; + + const { respond } = await import('${import.meta.resolve('./dev/ssr_entry.js')}'); + export { respond }; + + import.meta.hot?.accept(); + `; + } + } + } + } + }; + /** @type {Plugin} */ const plugin_virtual_modules = { name: 'vite-plugin-sveltekit-virtual-modules', @@ -512,7 +873,7 @@ function kit({ svelte_config }) { return `${runtime_directory}/client/remote-functions/index.js`; } - if (id.startsWith('__sveltekit/')) { + if (id.startsWith('__sveltekit/') && id !== '__sveltekit/dev-server-entry.js') { return `\0virtual:${id}`; } }, @@ -563,37 +924,37 @@ function kit({ svelte_config }) { return create_service_worker_module(svelte_config); case sveltekit_environment: { - const { version } = svelte_config.kit; + const { version } = kit; return dedent` - export const version = ${s(version.name)}; - export let building = false; - export let prerendering = false; + export const version = ${s(version.name)}; + export let building = false; + export let prerendering = false; - export function set_building() { - building = true; - } + export function set_building() { + building = true; + } - export function set_prerendering() { - prerendering = true; - } - `; + export function set_prerendering() { + prerendering = true; + } + `; } case sveltekit_server: { return dedent` - export let read_implementation = null; + export let read_implementation = null; - export let manifest = null; + export let manifest = null; - export function set_read_implementation(fn) { - read_implementation = fn; - } + export function set_read_implementation(fn) { + read_implementation = fn; + } - export function set_manifest(_) { - manifest = _; - } - `; + export function set_manifest(_) { + manifest = _; + } + `; } } } @@ -629,7 +990,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_lib, normalized_cwd); @@ -679,8 +1045,7 @@ function kit({ svelte_config }) { return; } - // in dev, this doesn't exist, so we need to create it - manifest_data ??= sync.all(svelte_config, vite_config_env.mode, root).manifest_data; + const manifest_data = dev_environment?.manifest_data ?? build_manifest_data; /** @type {Set} */ const entrypoints = new Set(); @@ -706,7 +1071,7 @@ function kit({ svelte_config }) { chain.push((current = candidates[0])); - includes_remote_file ||= svelte_config.kit.moduleExtensions.some((ext) => { + includes_remote_file ||= kit.moduleExtensions.some((ext) => { return current.endsWith(`.remote${ext}`); }); @@ -739,11 +1104,8 @@ function kit({ svelte_config }) { } }; - /** @type {ViteDevServer} */ - let dev_server; - /** @type {Array<{ hash: string, file: string }>} */ - const remotes = []; + let remotes = []; /** @type {Map} Maps remote hash -> original module id */ const remote_original_by_hash = new Map(); @@ -789,21 +1151,13 @@ function kit({ svelte_config }) { } }, - configureServer(_dev_server) { - if (!kit.experimental.remoteFunctions) { - return; - } - - dev_server = _dev_server; - }, - async transform(code, id) { if (!kit.experimental.remoteFunctions) { return; } const normalized = normalize_id(id, normalized_lib, normalized_cwd); - if (!svelte_config.kit.moduleExtensions.some((ext) => normalized.endsWith(`.remote${ext}`))) { + if (!kit.moduleExtensions.some((ext) => normalized.endsWith(`.remote${ext}`))) { return; } @@ -814,11 +1168,15 @@ function kit({ svelte_config }) { }; remotes.push(remote); + if (dev_environment) { + dev_environment.vite.environments.ssr.hot.send(`sveltekit:remotes`, remote); + invalidate_module(dev_environment.vite, sveltekit_remotes); + } if (this.environment.config.consumer !== 'client') { // we need to add an `await Promise.resolve()` because if the user imports this function // on the client AND in a load function when loading the client module we will trigger - // an ssrLoadModule during dev. During a link preload, the module can be mistakenly + // a ssrLoadModule during dev. During a link preload, the module can be mistakenly // loaded and transformed twice and the first time all its exports would be undefined // triggering a dev server error. By adding a microtask we ensure that the module is fully loaded @@ -829,7 +1187,7 @@ function kit({ svelte_config }) { import * as $$_self_$$ from './${path.basename(id)}'; import { init_remote_functions as $$_init_$$ } from '@sveltejs/kit/internal'; - ${dev_server ? 'await Promise.resolve()' : ''} + ${dev_environment?.vite ? 'await Promise.resolve()' : ''} $$_init_$$($$_self_$$, ${s(file)}, ${s(remote.hash)}); @@ -840,7 +1198,7 @@ function kit({ svelte_config }) { `; // Emit a dedicated entry chunk for this remote in SSR builds (prod only) - if (!dev_server) { + if (!dev_environment?.vite) { remote_original_by_hash.set(remote.hash, id); if (!emitted_remote_hashes.has(remote.hash)) { this.emitFile({ @@ -863,11 +1221,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_environment?.vite) { + /** @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_environment?.vite.environments.ssr.hot.off(event, listener); + }; + + dev_environment.vite.environments.ssr.hot.on(event, listener); + dev_environment.vite.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); } @@ -895,7 +1266,7 @@ function kit({ svelte_config }) { let result = `import * as ${namespace} from '__sveltekit/remote';\n\n${exports.join('\n')}\n`; - if (dev_server) { + if (dev_environment?.vite) { result += `\nimport.meta.hot?.accept();\n`; } @@ -905,19 +1276,19 @@ function kit({ svelte_config }) { } }; - /** @type {import('vite').Manifest} */ + /** @type {Manifest} */ let client_manifest; /** @type {import('types').Prerendered} */ let prerendered; - /** @type {Set} */ + /** @type {Set} client output and static files */ let build; /** @type {string} */ let service_worker_code; /** * Creates the service worker virtual modules - * @type {import('vite').Plugin} + * @type {Plugin} */ const plugin_service_worker = { name: 'vite-plugin-sveltekit-service-worker', @@ -957,7 +1328,7 @@ function kit({ svelte_config }) { ]; export const files = [ - ${manifest_data.assets + ${build_manifest_data.assets .filter((asset) => kit.serviceWorker.files(asset.file)) .map((asset) => `base + ${s(`/${asset.file}`)}`) .join(',\n')} @@ -991,14 +1362,13 @@ function kit({ svelte_config }) { } }; - /** @type {import('vite').Plugin} */ + /** @type {() => Promise} */ + let finalise; + + /** @type {Plugin} */ const plugin_compile = { name: 'vite-plugin-sveltekit-compile', - applyToEnvironment(environment) { - return environment.name !== 'serviceWorker'; - }, - /** * Build the SvelteKit-provided Vite config to be merged with the user's vite.config.js file. * @see https://vitejs.dev/guide/api-plugin.html#config @@ -1021,7 +1391,7 @@ function kit({ svelte_config }) { }; // add entry points for every endpoint... - manifest_data.routes.forEach((route) => { + build_manifest_data.routes.forEach((route) => { if (route.endpoint) { const resolved = path.resolve(root, route.endpoint.file); const relative = decodeURIComponent(path.relative(kit.files.routes, resolved)); @@ -1031,7 +1401,7 @@ function kit({ svelte_config }) { }); // ...and every component used by pages... - manifest_data.nodes.forEach((node) => { + build_manifest_data.nodes.forEach((node) => { for (const file of [node.component, node.universal, node.server]) { if (file) { const resolved = path.resolve(root, file); @@ -1046,30 +1416,29 @@ function kit({ svelte_config }) { }); // ...and every matcher - Object.entries(manifest_data.matchers).forEach(([key, file]) => { + Object.entries(build_manifest_data.matchers).forEach(([key, file]) => { const name = posixify(path.join('entries/matchers', key)); server_input[name] = path.resolve(root, file); }); // ...and the hooks files - if (manifest_data.hooks.server) { - server_input['entries/hooks.server'] = path.resolve(root, manifest_data.hooks.server); + if (build_manifest_data.hooks.server) { + server_input['entries/hooks.server'] = path.resolve( + root, + build_manifest_data.hooks.server + ); } - if (manifest_data.hooks.universal) { + if (build_manifest_data.hooks.universal) { server_input['entries/hooks.universal'] = path.resolve( root, - manifest_data.hooks.universal + build_manifest_data.hooks.universal ); } // ...and the server instrumentation file - const server_instrumentation = resolve_entry( - path.join(kit.files.src, 'instrumentation.server') - ); - if (server_instrumentation) { - const { adapter } = kit; + if (server_instrumentation_file) { if (adapter && !adapter.supports?.instrumentation?.()) { - throw new Error(`${server_instrumentation} is unsupported in ${adapter.name}.`); + throw new Error(`${server_instrumentation_file} is unsupported in ${adapter.name}.`); } if (!kit.experimental.instrumentation.server) { error_for_missing_config( @@ -1078,18 +1447,18 @@ function kit({ svelte_config }) { 'true' ); } - server_input['instrumentation.server'] = server_instrumentation; + server_input['instrumentation.server'] = server_instrumentation_file; } /** @type {Record} */ const client_input = {}; - if (svelte_config.kit.output.bundleStrategy !== 'split') { + if (kit.output.bundleStrategy !== 'split') { client_input['bundle'] = `${runtime_directory}/client/bundle.js`; } else { client_input['entry/start'] = `${runtime_directory}/client/entry.js`; client_input['entry/app'] = `${kit.outDir}/generated/client-optimized/app.js`; - manifest_data.nodes.forEach((node, i) => { + build_manifest_data.nodes.forEach((node, i) => { if (node.component || node.universal) { client_input[`nodes/${i}`] = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`; @@ -1097,7 +1466,7 @@ function kit({ svelte_config }) { }); } - const inline = svelte_config.kit.output.bundleStrategy === 'inline'; + const inline = kit.output.bundleStrategy === 'inline'; const config_base = assets_base(kit); @@ -1160,7 +1529,7 @@ function kit({ svelte_config }) { } } }, - // during the initial server build we don't know yet + // these are stubs that will be replaced after the initial server build define: { __SVELTEKIT_HAS_SERVER_LOAD__: 'true', __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true', @@ -1176,7 +1545,7 @@ function kit({ svelte_config }) { format: inline ? 'iife' : 'esm', entryFileNames: `${prefix}/[name].[hash].js`, chunkFileNames: `${prefix}/chunks/[hash].js`, - codeSplitting: svelte_config.kit.output.bundleStrategy === 'split' + codeSplitting: kit.output.bundleStrategy === 'split' }, // This silences Rolldown warnings about not supporting `import.meta` // for the `iife` output format. We don't care because it's @@ -1232,7 +1601,7 @@ function kit({ svelte_config }) { }; if (service_worker_entry_file) { - /** @type {Record} */ ( + /** @type {Record} */ ( new_config.environments ).serviceWorker = { build: { @@ -1280,8 +1649,15 @@ 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(vite) { - return await dev(vite, vite_config, svelte_config, () => remotes, root); + configureServer(vite) { + // other properties will be populated after running the `dev` function below + dev_environment = /** @type {import('types').DevEnvironment} */ ({ + vite + }); + + vite.environments.ssr.hot.on('sveltekit:ssr-load-module-error', display_ssr_error_on_client); + + return dev(vite, vite_config, svelte_config, root, dev_environment, adapter); }, /** @@ -1289,9 +1665,15 @@ function kit({ svelte_config }) { * @see https://vitejs.dev/guide/api-plugin.html#configurepreviewserver */ configurePreviewServer(vite) { + if (adapter?.vite?.plugins) return; + return preview(vite, vite_config, svelte_config); }, + applyToEnvironment(environment) { + return environment.name !== 'serviceWorker'; + }, + renderChunk(code, chunk) { if (code.includes('__SVELTEKIT_TRACK__')) { return { @@ -1323,21 +1705,22 @@ function kit({ svelte_config }) { } mkdirp(out); - const server_bundle = /** @type {import('vite').Rolldown.RolldownOutput} */ ( + const server_bundle = /** @type {Rolldown.RolldownOutput} */ ( await builder.build(builder.environments.ssr) ); - const verbose = vite_config.logLevel === 'info'; + const verbose = builder.config.logLevel === 'info'; const log = logger({ verbose }); - /** @type {import('vite').Manifest} */ + /** @type {Manifest} */ const server_manifest = JSON.parse(read(`${out}/server/.vite/manifest.json`)); /** @type {import('types').BuildData} */ const build_data = { app_dir: kit.appDir, app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, - manifest_data, + base: kit.paths.base, + manifest_data: build_manifest_data, out_dir: out, service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? client: null, @@ -1351,7 +1734,7 @@ function kit({ svelte_config }) { build_data, prerendered: [], relative_path: '.', - routes: manifest_data.routes, + routes: build_manifest_data.routes, remotes, root })};\n` @@ -1360,15 +1743,11 @@ function kit({ svelte_config }) { log.info('Analysing routes'); const { metadata } = await analyse({ - hash: kit.router.type === 'hash', manifest_path, - manifest_data, + manifest_data: build_manifest_data, server_manifest, tracked_features, - env: { ...env.private, ...env.public }, out, - output_config: svelte_config.output, - remotes, root }); @@ -1379,7 +1758,7 @@ function kit({ svelte_config }) { // create client build write_client_manifest( kit, - manifest_data, + build_manifest_data, `${kit.outDir}/generated/client-optimized`, metadata.nodes ); @@ -1399,7 +1778,7 @@ function kit({ svelte_config }) { s(has_universal_load); } - const { output: client_chunks } = /** @type {import('vite').Rolldown.RolldownOutput} */ ( + const { output: client_chunks } = /** @type {Rolldown.RolldownOutput} */ ( await builder.build(builder.environments.client) ); @@ -1442,7 +1821,7 @@ function kit({ svelte_config }) { } } - /** @type {import('vite').Manifest} */ + /** @type {Manifest} */ client_manifest = JSON.parse(read(`${out}/client/.vite/manifest.json`)); /** @@ -1452,7 +1831,7 @@ function kit({ svelte_config }) { const deps_of = (entry, add_dynamic_css = false) => find_deps(client_manifest, posixify(path.relative(root, entry)), add_dynamic_css, root); - if (svelte_config.kit.output.bundleStrategy === 'split') { + if (kit.output.bundleStrategy === 'split') { const start = deps_of(`${runtime_directory}/client/entry.js`); const app = deps_of(`${kit.outDir}/generated/client-optimized/app.js`); @@ -1470,8 +1849,8 @@ function kit({ svelte_config }) { // In case of server-side route resolution, we create a purpose-built route manifest that is // similar to that on the client, with as much information computed upfront so that we // don't need to include any code of the actual routes in the server bundle. - if (svelte_config.kit.router.resolution === 'server') { - const nodes = manifest_data.nodes.map((node, i) => { + if (kit.router.resolution === 'server') { + const nodes = build_manifest_data.nodes.map((node, i) => { if (node.component || node.universal) { const entry = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`; const deps = deps_of(entry, true); @@ -1488,7 +1867,7 @@ function kit({ svelte_config }) { build_data.client.css = nodes.map((node) => node?.css); build_data.client.routes = compact( - manifest_data.routes.map((route) => { + build_manifest_data.routes.map((route) => { if (!route.page) return; return { @@ -1517,8 +1896,8 @@ function kit({ svelte_config }) { ) }; - if (svelte_config.kit.output.bundleStrategy === 'inline') { - const style = /** @type {import('vite').Rolldown.OutputAsset} */ ( + if (kit.output.bundleStrategy === 'inline') { + const style = /** @type {Rolldown.OutputAsset} */ ( client_chunks.find( (chunk) => chunk.type === 'asset' && chunk.names.length === 1 && chunk.names[0] === 'style.css' @@ -1532,6 +1911,9 @@ function kit({ svelte_config }) { } } + // deduplicate remotes because the same hash may be pushed from both the client and server builds + remotes = Array.from(new Set(remotes)); + // regenerate manifest now that we have client entry... fs.writeFileSync( manifest_path, @@ -1539,33 +1921,32 @@ function kit({ svelte_config }) { build_data, prerendered: [], relative_path: '.', - routes: manifest_data.routes, + routes: build_manifest_data.routes, remotes, root })};\n` ); // regenerate nodes with the client manifest... - build_server_nodes( + build_server_nodes({ out, - kit, - manifest_data, + manifest_data: build_manifest_data, server_manifest, client_manifest, + paths: kit.paths, + inline_style_threshold: kit.inlineStyleThreshold, assets_path, client_chunks, - svelte_config.kit.output, + output_config: kit.output, root - ); + }); // ...and prerender const prerender_results = await prerender({ - hash: kit.router.type === 'hash', out, manifest_path, metadata, verbose, - env: { ...env.private, ...env.public }, root }); prerendered = prerender_results.prerendered; @@ -1577,7 +1958,7 @@ function kit({ svelte_config }) { metadata, cwd, server_bundle, - vite_config.build.sourcemap + builder.config.build.sourcemap ); // generate a new manifest that doesn't include prerendered pages @@ -1587,7 +1968,7 @@ function kit({ svelte_config }) { build_data, prerendered: prerendered.paths, relative_path: '.', - routes: manifest_data.routes.filter( + routes: build_manifest_data.routes.filter( (route) => prerender_results.prerender_map.get(route.id) !== true ), remotes, @@ -1595,62 +1976,93 @@ function kit({ svelte_config }) { })};\n` ); - if (service_worker_entry_file) { - if (kit.paths.assets) { - throw new Error('Cannot use service worker alongside config.kit.paths.assets'); - } + // defer the adapt step to run after any buildApp hooks the adapter might have + finalise = async () => { + // defer creating the service worker too because other plugins might build + // the client environment again and overwrite our service worker which + // outputs to the same directory + if (service_worker_entry_file) { + if (kit.paths.assets) { + throw new Error('Cannot use service worker alongside config.kit.paths.assets'); + } - log.info('Building service worker'); + log.info('Building service worker'); - builder.environments.serviceWorker.config.define = - builder.environments.client.config.define; - builder.environments.serviceWorker.config.resolve.alias = [ - ...get_config_aliases(kit, vite_config.root) - ]; - builder.environments.serviceWorker.config.experimental.renderBuiltUrl = (filename) => { - return { - runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname` + builder.environments.serviceWorker.config.define = + builder.environments.client.config.define; + builder.environments.serviceWorker.config.resolve.alias = [ + ...get_config_aliases(kit, builder.config.root) + ]; + builder.environments.serviceWorker.config.experimental.renderBuiltUrl = (filename) => { + return { + runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname` + }; }; - }; - - await builder.build(builder.environments.serviceWorker); - } - console.log( - `\nRun ${styleText(['bold', 'cyan'], 'npm run preview')} to preview your production build locally.` - ); - - if (kit.adapter) { - const { adapt } = await import('../../core/adapt/index.js'); - await adapt( - svelte_config, - build_data, - metadata, - prerendered, - prerender_results.prerender_map, - log, - remotes, - vite_config - ); - } else { - console.log(styleText(['bold', 'yellow'], '\nNo adapter specified')); + await builder.build(builder.environments.serviceWorker); + } - const link = styleText(['bold', 'cyan'], 'https://svelte.dev/docs/kit/adapters'); console.log( - `See ${link} to learn how to configure your app to run on the platform of your choosing` + `\nRun ${styleText(['bold', 'cyan'], 'npm run preview')} to preview your production build locally.` ); + + if (adapter) { + const { adapt } = await import('../../core/adapt/index.js'); + await adapt( + adapter, + svelte_config, + build_data, + metadata, + prerendered, + prerender_results.prerender_map, + log, + remotes, + builder.config, + out + ); + } else { + console.log(styleText(['bold', 'yellow'], '\nNo adapter specified')); + + const link = styleText(['bold', 'cyan'], 'https://svelte.dev/docs/kit/adapters'); + console.log( + `See ${link} to learn how to configure your app to run on the platform of your choosing` + ); + } + }; + } + }; + + /** @type {Plugin} */ + const plugin_adapter = { + name: 'vite-plugin-sveltekit-adapter', + apply: 'build', + buildApp: { + // this will run after any buildApp hooks provided by other Vite plugins + // see https://vite.dev/guide/api-environment-frameworks#environments-during-build + order: 'post', + async handler() { + await finalise(); } + }, + // add it here so that we can retrieve from a separate process by resolving the Vite config + api: { + adapter } }; return [ plugin_setup, + adapter?.vite?.plugins ? undefined : plugin_node_environment, plugin_remote, + plugin_server_filesystem, + plugin_dev_ssr, plugin_virtual_modules, process.env.TEST !== 'true' ? plugin_guard : undefined, plugin_service_worker, - plugin_compile - ].filter((p) => !!p); + plugin_compile, + plugin_adapter, + adapter?.vite?.plugins + ].filter(Boolean); } /** @@ -1699,20 +2111,46 @@ function find_overridden_config(config, resolved_config, enforced_config, path, } /** - * @param {import('types').ValidatedConfig} config + * @param {ValidatedConfig} config + * @returns {string} */ -const create_service_worker_module = (config) => dedent` - if (typeof self === 'undefined' || self instanceof ServiceWorkerGlobalScope === false) { - throw new Error('This module can only be imported inside a service worker'); +function create_service_worker_module(config) { + return dedent` + if (typeof self === 'undefined' || self instanceof ServiceWorkerGlobalScope === false) { + throw new Error('This module can only be imported inside a service worker'); + } + + export const build = []; + export const files = [ + ${create_assets(config) + .filter((asset) => config.kit.serviceWorker.files(asset.file)) + .map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`) + .join(',\n')} + ]; + export const prerendered = []; + export const version = ${s(config.kit.version.name)}; + `; +} + +/** @type {(error: Error) => void} */ +function display_ssr_error_on_client(err) { + const vite = dev_environment?.vite; + if (!vite) return; + + const msg = buildErrorMessage(err, [styleText('red', `Internal server error: ${err.message}`)]); + + if (!vite.config.logger.hasErrorLogged(err)) { + vite.config.logger.error(msg, { error: err }); } - export const build = []; - export const files = [ - ${create_assets(config) - .filter((asset) => config.kit.serviceWorker.files(asset.file)) - .map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`) - .join(',\n')} - ]; - export const prerendered = []; - export const version = ${s(config.kit.version.name)}; -`; + vite.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/module_ids.js b/packages/kit/src/exports/vite/module_ids.js index 69325f3edf41..d0829403b88d 100644 --- a/packages/kit/src/exports/vite/module_ids.js +++ b/packages/kit/src/exports/vite/module_ids.js @@ -1,5 +1,5 @@ import { fileURLToPath } from 'node:url'; -import { posixify } from '../../utils/filesystem.js'; +import { posixify } from '../../utils/os.js'; export const env_static_private = '\0virtual:env/static/private'; export const env_static_public = '\0virtual:env/static/public'; @@ -10,6 +10,11 @@ export const service_worker = '\0virtual:service-worker'; export const sveltekit_environment = '\0virtual:__sveltekit/environment'; export const sveltekit_server = '\0virtual:__sveltekit/server'; +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_ipc = '\0virtual:__sveltekit/ipc'; +export const sveltekit_env = '\0virtual:sveltekit:env'; export const app_server = posixify( fileURLToPath(new URL('../../runtime/app/server/index.js', import.meta.url)) diff --git a/packages/kit/src/exports/vite/options.js b/packages/kit/src/exports/vite/options.js new file mode 100644 index 000000000000..461c6222bd2f --- /dev/null +++ b/packages/kit/src/exports/vite/options.js @@ -0,0 +1,17 @@ +/** @import { Validator } from '../../core/config/types.js' */ + +import { object, validate } from '../../core/config/options.js'; + +/** @type {Validator} */ +const options = object({ + adapter: validate(null, (input, keypath) => { + if (typeof input !== 'object' || !input.adapt) { + const message = `${keypath} should be an object with an "adapt" method`; + throw new Error(`${message}. See https://svelte.dev/docs/kit/adapters`); + } + + return input; + }) +}); + +export default options; diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 71b017c046b8..76b60181c2e4 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -53,8 +53,6 @@ export async function preview(vite, vite_config, svelte_config) { read: (file) => createReadableStream(`${dir}/${file}`) }); - 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 @@ -214,8 +212,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..1276f53cf8d1 100644 --- a/packages/kit/src/exports/vite/types.d.ts +++ b/packages/kit/src/exports/vite/types.d.ts @@ -1,3 +1,35 @@ +import 'vite/types/customEvent.d.ts'; +import type { PageOptions } from './static_analysis/index.js'; + +declare module 'vite/types/customEvent.d.ts' { + interface CustomEventMap { + 'sveltekit:port': number; + '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; + 'sveltekit:prerender-assets': string; + } +} + +export interface SerialisedResponse { + status: number; + statusText: string; + headers: Record; + body: ArrayBuffer; +} + 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 46e5b48576f2..e2bbcc878856 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -1,9 +1,9 @@ import path from 'node:path'; import { loadEnv } from 'vite'; -import { posixify } from '../../utils/filesystem.js'; +import { posixify } from '../../utils/os.js'; import { negotiate } from '../../utils/http.js'; import { filter_env } from '../../utils/env.js'; -import { escape_html } from '../../utils/escape.js'; +import { escape_for_regexp, escape_html } from '../../utils/escape.js'; import { dedent } from '../../core/sync/utils.js'; import { app_server, @@ -55,13 +55,6 @@ export function get_config_aliases(config, root) { return alias; } -/** - * @param {string} str - */ -function escape_for_regexp(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match); -} - /** * Load environment variables from process.env and .env files * @param {import('types').ValidatedKitConfig['env']} env_config diff --git a/packages/kit/src/exports/vite/utils.spec.js b/packages/kit/src/exports/vite/utils.spec.js index 7878487573fe..8512ecc081ae 100644 --- a/packages/kit/src/exports/vite/utils.spec.js +++ b/packages/kit/src/exports/vite/utils.spec.js @@ -1,7 +1,7 @@ import path from 'node:path'; import { expect, test } from 'vitest'; import { validate_config } from '../../core/config/index.js'; -import { posixify } from '../../utils/filesystem.js'; +import { posixify } from '../../utils/os.js'; import { dedent } from '../../core/sync/utils.js'; import { get_config_aliases, error_for_missing_config } from './utils.js'; diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 43fe32706a12..cc4769fdbe80 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -5,8 +5,6 @@ import { version } from '__sveltekit/environment'; import { noop } from '../../utils/functions.js'; import { PRELOAD_PRIORITIES } from './constants.js'; -/* global __SVELTEKIT_APP_VERSION_FILE__, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__ */ - export const origin = BROWSER ? location.origin : ''; /** @param {string | URL} url */ diff --git a/packages/kit/src/runtime/server/ambient.d.ts b/packages/kit/src/runtime/server/ambient.d.ts index 2f38261123dc..09c2db316f26 100644 --- a/packages/kit/src/runtime/server/ambient.d.ts +++ b/packages/kit/src/runtime/server/ambient.d.ts @@ -1,4 +1,10 @@ declare module '__SERVER__/internal.js' { export const options: import('types').SSROptions; export const get_hooks: () => Promise>; + export const set_manifest: (manifest: import('@sveltejs/kit').SSRManifest) => void; + export const set_read_implementation: (read: (file: string) => ReadableStream) => void; + export const set_private_env: (env: Record) => void; + export const set_public_env: (env: Record) => void; + export const set_building: () => void; + export const set_prerendering: () => void; } diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index baf93f1b507f..b010dd7589b7 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -99,7 +99,7 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade ? manifest.mimeTypes[filename.slice(filename.lastIndexOf('.'))] : 'text/html'; - return new Response(state.read(file), { + return new Response(await state.read(file), { headers: type ? { 'content-type': type } : {} }); } else if (read_implementation && file in manifest._.server_assets) { diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index bab755e3408f..5778f341930e 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -8,6 +8,7 @@ import { filter_env } from '../../utils/env.js'; import { format_server_error } from './utils.js'; import { set_read_implementation, set_manifest } from '__sveltekit/server'; import { set_app } from './app.js'; +import { create_synchronous_read } from './read.js'; /** @type {Promise} */ let init_promise; @@ -65,41 +66,7 @@ export class Server { set_public_env(filter_env(env, env_public_prefix, env_private_prefix)); if (read) { - // Wrap the read function to handle MaybePromise - // and ensure the public API stays synchronous - /** @param {string} file */ - const wrapped_read = (file) => { - const result = read(file); - if (result instanceof ReadableStream) { - return result; - } else { - return new ReadableStream({ - async start(controller) { - try { - const stream = await Promise.resolve(result); - if (!stream) { - controller.close(); - return; - } - - const reader = stream.getReader(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - controller.enqueue(value); - } - - controller.close(); - } catch (error) { - controller.error(error); - } - } - }); - } - }; - - set_read_implementation(wrapped_read); + set_read_implementation(create_synchronous_read(read)); } // During DEV and for some adapters this function might be called in quick succession, diff --git a/packages/kit/src/runtime/server/page/csp.spec.js b/packages/kit/src/runtime/server/page/csp.spec.js index 195a995e8f77..df8f0bf458ba 100644 --- a/packages/kit/src/runtime/server/page/csp.spec.js +++ b/packages/kit/src/runtime/server/page/csp.spec.js @@ -37,6 +37,7 @@ describe.skipIf(process.env.NODE_ENV === 'production')('CSPs in dev', () => { ); }); + // TODO: re-enable when we support strict-dynamic in dev again test.skip('removes strict-dynamic', () => { ['default-src', 'script-src'].forEach((name) => { const csp = new Csp( diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index c958c9cb8999..3c090c4c28a0 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -291,7 +291,10 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) if (same_origin) { if (state.prerendering) { - dependency = { response, body: null }; + // the prerender code needs to read and serialise the response body + // from the environment to the Vite process so we clone the response + // to ensure its body remains unused until then + dependency = { response: response.clone(), body: null }; state.prerendering.dependencies.set(url.pathname, dependency); } } else if (url.protocol === 'https:' || url.protocol === 'http:') { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index b0580892e680..a7eddc24a9d3 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -623,8 +623,11 @@ export async function render_response({ }`); } + // we need to eagerly import the Vite client module in development to ensure + // that Vite global constant replacements are initialised before our code runs const init_app = ` { + ${DEV ? `import('${paths.base}/@vite/client')` : ''} ${blocks.join('\n\n\t\t\t\t\t')} } `; diff --git a/packages/kit/src/runtime/server/read.js b/packages/kit/src/runtime/server/read.js new file mode 100644 index 000000000000..31801e0a80f6 --- /dev/null +++ b/packages/kit/src/runtime/server/read.js @@ -0,0 +1,39 @@ +/** + * Wrap the `read` function to handle `MaybePromise` + * and ensure the public API stays synchronous + * @param {NonNullable} read + * @returns {(path: string) => ReadableStream} + */ +export function create_synchronous_read(read) { + return (file) => { + const result = read(file); + if (result instanceof ReadableStream) { + return result; + } else { + return new ReadableStream({ + async start(controller) { + try { + const stream = await Promise.resolve(result); + if (!stream) { + controller.close(); + return; + } + + const reader = stream.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + controller.enqueue(value); + } + + controller.close(); + } catch (error) { + // TODO: we should throw here even if the user doesn't try to read the response body + controller.error(error); + } + } + }); + } + }; +} diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 0385a458bec6..799d70d51c69 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -40,8 +40,6 @@ import { get_remote_id, handle_remote_call } from './remote.js'; import { record_span } from '../telemetry/record_span.js'; import { otel } from '../telemetry/otel.js'; -/* global __SVELTEKIT_ADAPTER_NAME__ */ - /** @type {import('types').RequiredResolveOptions['transformPageChunk']} */ const default_transform = ({ html }) => html; @@ -222,13 +220,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 - }); - } - let resolved_path = url.pathname; if (!remote_id) { @@ -379,7 +370,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} */ @@ -395,130 +386,136 @@ export async function internal_respond(request, options, manifest, state) { } if (state.before_handle) { - state.before_handle(event, config, prerender); - } - - if (state.emulator?.platform) { - event.platform = await state.emulator.platform({ config, prerender }); + return await state.before_handle(event, config, prerender, handle); } } } - set_trailing_slash(trailing_slash); - - if (state.prerendering && !state.prerendering.fallback && !state.prerendering.inside_reroute) { - disable_search(url); - } + async function handle() { + set_trailing_slash(trailing_slash); - const response = await record_span({ - name: 'sveltekit.handle.root', - attributes: { - 'http.route': event.route.id || 'unknown', - 'http.method': event.request.method, - 'http.url': event.url.href, - 'sveltekit.is_data_request': is_data_request, - 'sveltekit.is_sub_request': event.isSubRequest - }, - fn: async (root_span) => { - const traced_event = { - ...event, - tracing: { - enabled: __SVELTEKIT_SERVER_TRACING_ENABLED__, - root: root_span, - current: root_span - } - }; - - return await with_request_store({ event: traced_event, state: event_state }, () => - options.hooks.handle({ - event: traced_event, - resolve: (event, opts) => { - return record_span({ - name: 'sveltekit.resolve', - attributes: { - 'http.route': event.route.id || 'unknown' - }, - fn: (resolve_span) => { - // counter-intuitively, we need to clear the event, so that it's not - // e.g. accessible when loading modules needed to handle the request - return with_request_store(null, () => - resolve(merge_tracing(event, resolve_span), page_nodes, opts).then( - (response) => { - // add headers/cookies here, rather than inside `resolve`, so that we - // can do it once for all responses instead of once per `return` - for (const key in headers) { - const value = headers[key]; - response.headers.set(key, /** @type {string} */ (value)); - } - - add_cookies_to_headers(response.headers, new_cookies.values()); + if ( + state.prerendering && + !state.prerendering.fallback && + !state.prerendering.inside_reroute + ) { + disable_search(url); + } - if (state.prerendering && event.route.id !== null) { - response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); + const response = await record_span({ + name: 'sveltekit.handle.root', + attributes: { + 'http.route': event.route.id || 'unknown', + 'http.method': event.request.method, + 'http.url': event.url.href, + 'sveltekit.is_data_request': is_data_request, + 'sveltekit.is_sub_request': event.isSubRequest + }, + fn: async (root_span) => { + const traced_event = { + ...event, + tracing: { + enabled: __SVELTEKIT_SERVER_TRACING_ENABLED__, + root: root_span, + current: root_span + } + }; + + return await with_request_store({ event: traced_event, state: event_state }, () => + options.hooks.handle({ + event: traced_event, + resolve: (event, opts) => { + return record_span({ + name: 'sveltekit.resolve', + attributes: { + 'http.route': event.route.id || 'unknown' + }, + fn: (resolve_span) => { + // counter-intuitively, we need to clear the event, so that it's not + // e.g. accessible when loading modules needed to handle the request + return with_request_store(null, () => + resolve(merge_tracing(event, resolve_span), page_nodes, opts).then( + (response) => { + // add headers/cookies here, rather than inside `resolve`, so that we + // can do it once for all responses instead of once per `return` + for (const key in headers) { + const value = headers[key]; + response.headers.set(key, /** @type {string} */ (value)); + } + + add_cookies_to_headers(response.headers, new_cookies.values()); + + if (state.prerendering && event.route.id !== null) { + response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); + } + + resolve_span.setAttributes({ + 'http.response.status_code': response.status, + 'http.response.body.size': + response.headers.get('content-length') || 'unknown' + }); + + return response; } + ) + ); + } + }); + } + }) + ); + } + }); - resolve_span.setAttributes({ - 'http.response.status_code': response.status, - 'http.response.body.size': - response.headers.get('content-length') || 'unknown' - }); + // respond with 304 if etag matches + if (response.status === 200 && response.headers.has('etag')) { + let if_none_match_value = request.headers.get('if-none-match'); - return response; - } - ) - ); - } - }); - } - }) - ); - } - }); + // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives + if (if_none_match_value?.startsWith('W/"')) { + if_none_match_value = if_none_match_value.substring(2); + } - // respond with 304 if etag matches - if (response.status === 200 && response.headers.has('etag')) { - let if_none_match_value = request.headers.get('if-none-match'); + const etag = /** @type {string} */ (response.headers.get('etag')); + + if (if_none_match_value === etag) { + const headers = new Headers({ etag }); + + // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie + for (const key of [ + 'cache-control', + 'content-location', + 'date', + 'expires', + 'vary', + 'set-cookie' + ]) { + const value = response.headers.get(key); + if (value) headers.set(key, value); + } - // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives - if (if_none_match_value?.startsWith('W/"')) { - if_none_match_value = if_none_match_value.substring(2); + return new Response(undefined, { + status: 304, + headers + }); + } } - const etag = /** @type {string} */ (response.headers.get('etag')); - - if (if_none_match_value === etag) { - const headers = new Headers({ etag }); - - // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie - for (const key of [ - 'cache-control', - 'content-location', - 'date', - 'expires', - 'vary', - 'set-cookie' - ]) { - const value = response.headers.get(key); - if (value) headers.set(key, value); + // Edge case: If user does `return Response(30x)` in handle hook while processing a data request, + // we need to transform the redirect response to a corresponding JSON response. + if (is_data_request && response.status >= 300 && response.status <= 308) { + const location = response.headers.get('location'); + if (location) { + return redirect_json_response( + new Redirect(/** @type {any} */ (response.status), location) + ); } - - return new Response(undefined, { - status: 304, - headers - }); } - } - // Edge case: If user does `return Response(30x)` in handle hook while processing a data request, - // we need to transform the redirect response to a corresponding JSON response. - if (is_data_request && response.status >= 300 && response.status <= 308) { - const location = response.headers.get('location'); - if (location) { - return redirect_json_response(new Redirect(/** @type {any} */ (response.status), location)); - } + return response; } - return response; + return await handle(); } catch (e) { if (e instanceof Redirect) { try { diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 7f7e8822df01..526a2e15f939 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -4,9 +4,9 @@ import { HttpError } from '@sveltejs/kit/internal'; import { with_request_store } from '@sveltejs/kit/internal/server'; import { coalesce_to_error, get_message, get_status } from '../../utils/error.js'; import { negotiate } from '../../utils/http.js'; -import { fix_stack_trace } from '../shared-server.js'; import { ENDPOINT_METHODS } from '../../constants.js'; import { escape_html } from '../../utils/escape.js'; +import * as path from '../../utils/path.js'; /** * @param {Partial>} mod @@ -103,10 +103,6 @@ export async function handle_error_and_jsonify(event, state, options, error) { return { message: 'Unknown Error', ...error.body }; } - if (DEV && typeof error == 'object') { - fix_stack_trace(error); - } - const status = get_status(error); const message = get_message(error); @@ -210,9 +206,8 @@ let relative = (file) => file; if (DEV) { try { - const path = await import('node:path'); - - relative = (file) => path.relative('.', file); + const root = typeof __SVELTEKIT_ROOT__ === 'string' ? __SVELTEKIT_ROOT__ : ''; + relative = (file) => path.relative(root, file); } catch { // do nothing } diff --git a/packages/kit/src/runtime/shared-server.js b/packages/kit/src/runtime/shared-server.js index 5a535448449f..4f89379a54f0 100644 --- a/packages/kit/src/runtime/shared-server.js +++ b/packages/kit/src/runtime/shared-server.js @@ -10,9 +10,6 @@ export let private_env = {}; */ export let public_env = {}; -/** @param {any} error */ -export let fix_stack_trace = (error) => error?.stack; - /** @type {(environment: Record) => void} */ export function set_private_env(environment) { private_env = environment; @@ -22,8 +19,3 @@ export function set_private_env(environment) { export function set_public_env(environment) { public_env = environment; } - -/** @param {(error: Error) => string} value */ -export function set_fix_stack_trace(value) { - fix_stack_trace = value; -} diff --git a/packages/kit/src/runtime/utils.js b/packages/kit/src/runtime/utils.js index f19967160a9b..8205ed35b7af 100644 --- a/packages/kit/src/runtime/utils.js +++ b/packages/kit/src/runtime/utils.js @@ -1,4 +1,17 @@ import { BROWSER } from 'esm-env'; +import { posixify } from '../utils/os.js'; + +/** + * Resolved path of the `runtime` directory + * + * TODO Windows issue: + * Vite or sth else somehow sets the driver letter inconsistently to lower or upper case depending on the run environment. + * In playwright debug mode run through VS Code this a root-to-lowercase conversion is needed in order for the tests to run. + * If we do this conversion in other cases it has the opposite effect though and fails. + */ +// import.meta.dirname doesn't exist on the client so we need to avoid running +// posixify to avoid a runtime error when it's undefined +export const runtime_directory = import.meta.dirname ? posixify(import.meta.dirname) : ''; export const text_encoder = new TextEncoder(); diff --git a/packages/kit/src/types/ambient-private.d.ts b/packages/kit/src/types/ambient-private.d.ts index c98af8cb0062..753248b1d785 100644 --- a/packages/kit/src/types/ambient-private.d.ts +++ b/packages/kit/src/types/ambient-private.d.ts @@ -27,3 +27,27 @@ declare module '__sveltekit/server' { export function set_manifest(manifest: SSRManifest): void; 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; +} diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index b08a2f963160..896c91c4b4af 100644 --- a/packages/kit/src/types/ambient.d.ts +++ b/packages/kit/src/types/ambient.d.ts @@ -31,6 +31,7 @@ declare namespace App { /** * The interface that defines `event.locals`, which can be accessed in server [hooks](https://svelte.dev/docs/kit/hooks) (`handle`, and `handleError`), server-only `load` functions, and `+server.js` files. */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface Locals {} /** @@ -38,16 +39,19 @@ declare namespace App { * The `Load` and `ServerLoad` functions in `./$types` will be narrowed accordingly. * Use optional properties for data that is only present on specific pages. Do not add an index signature (`[key: string]: any`). */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface PageData {} /** * The shape of the `page.state` object, which can be manipulated using the [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) functions from `$app/navigation`. */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface PageState {} /** * If your adapter provides [platform-specific context](https://svelte.dev/docs/kit/adapters#Platform-specific-context) via `event.platform`, you can specify it here. */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface Platform {} } @@ -146,3 +150,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 4b442f89944c..3458e62ab20f 100644 --- a/packages/kit/src/types/global-private.d.ts +++ b/packages/kit/src/types/global-private.d.ts @@ -42,6 +42,10 @@ declare global { /** Resolve a placeholder promise */ resolve?: (data: { id: number; data: any; error: any }) => void; }; + /** Allows us to resolve paths relative to the Vite root setting during development */ + const __SVELTEKIT_ROOT__: string; + const __SVELTEKIT_OUT_DIR__: string; + const __SVELTEKIT_FILES_ASSETS__: string; /** * This makes the use of specific features visible at both dev and build time, in such a * way that we can error when they are not supported by the target platform. diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index cc816298d1cc..0e48c59bed27 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, Adapter, ServerInit, ClientInit, @@ -35,6 +34,7 @@ import { } from './private.js'; import { Span } from '@opentelemetry/api'; import type { PageOptions } from '../exports/vite/static_analysis/index.js'; +import type { ViteDevServer } from 'vite'; export interface ServerModule { Server: typeof InternalServer; @@ -49,7 +49,6 @@ export interface ServerInternalModule { set_public_env(environment: Record): void; set_read_implementation(implementation: (path: string) => ReadableStream): void; set_version(version: string): void; - set_fix_stack_trace(fix_stack_trace: (error: unknown) => string): void; get_hooks: () => Promise>; } @@ -69,8 +68,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; @@ -179,14 +181,22 @@ export class InternalServer extends Server { request: Request, options: RequestOptions & { prerendering?: PrerenderOptions; - read: (file: string) => NonSharedBuffer; - /** A hook called before `handle` during dev, so that `AsyncLocalStorage` can be populated. */ - before_handle?: (event: RequestEvent, config: any, prerender: PrerenderOption) => void; - emulator?: Emulator; + /** @internal for saving dependencies during prerendering and generating fallback pages */ + read: (file: string) => MaybePromise>; + /** @internal used during development to check feature availability depending on the current route */ + before_handle?: ( + event: RequestEvent, + config: any, + prerender: PrerenderOption, + handle: () => Promise + ) => Promise; } ): Promise; } +/** + * Used to construct the SSR manifest + */ export interface ManifestData { /** Static files from `kit.config.files.assets`. */ assets: Asset[]; @@ -410,6 +420,7 @@ export interface ServerMetadata { routes: Map; /** For each hashed remote file, a map of export name -> { type, dynamic }, where `dynamic` is `false` for non-dynamic prerender functions */ remotes: Map>; + remotes_with_prerender: Set; } export interface SSRComponent { @@ -564,13 +575,20 @@ export interface SSRState { * prerender option is inherited by the endpoint, unless overridden. */ prerender_default?: PrerenderOption; - read?: (file: string) => NonSharedBuffer; + /** + * @internal reads from the filesystem when user code tries to fetch a static asset + */ + read?: (file: string) => MaybePromise>; /** * Used to set up `__SVELTEKIT_TRACK__` which checks if a used feature is supported. * E.g. if `read` from `$app/server` is used, it checks whether the route's config is compatible. */ - before_handle?: (event: RequestEvent, config: any, prerender: PrerenderOption) => void; - emulator?: Emulator; + before_handle?: ( + event: RequestEvent, + config: any, + prerender: PrerenderOption, + handle: () => Promise + ) => Promise; } export type StrictBody = string | ArrayBufferView; @@ -589,9 +607,8 @@ export type ValidatedConfig = Config & { extensions: string[]; }; -export type ValidatedKitConfig = Omit, 'adapter'> & { - adapter?: Adapter; -}; +// TODO: remove the omit in 4.0 +export type ValidatedKitConfig = Omit, 'adapter'>; export type BinaryFormMeta = { remote_refreshes?: string[]; @@ -714,5 +731,11 @@ export interface RequestStore { state: RequestState; } +export interface DevEnvironment { + vite: ViteDevServer; + /** used to construct the SSR manifest */ + manifest_data: ManifestData; +} + export * from '../exports/index.js'; export * from './private.js'; diff --git a/packages/kit/src/utils/escape.js b/packages/kit/src/utils/escape.js index 3feaafae6fc8..bf3b660b21a6 100644 --- a/packages/kit/src/utils/escape.js +++ b/packages/kit/src/utils/escape.js @@ -81,3 +81,11 @@ export function escape_for_interpolation(str, replacements) { } return escaped; } + +// TODO: replace this with RegExp.escape when we make Node 24 the minimum +/** + * @param {string} str + */ +export function escape_for_regexp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match); +} diff --git a/packages/kit/src/utils/features.js b/packages/kit/src/utils/features.js index 4a8530d22bbb..3662a3e05dc9 100644 --- a/packages/kit/src/utils/features.js +++ b/packages/kit/src/utils/features.js @@ -1,24 +1,22 @@ +import { get } from '__sveltekit/ipc'; + /** * @param {string} route_id - * @param {any} config + * @param {unknown} config * @param {string} feature - * @param {import('@sveltejs/kit').Adapter | undefined} adapter */ -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 - }); +export async function check_feature(route_id, config, feature) { + const response = await get( + `/check-feature?${new URLSearchParams({ route_id, config: JSON.stringify(config), feature })}` + ); + if (!response.ok) { + throw new Error( + `Failed to check feature ${feature} for route ${route_id}: ${response.status} ${response.statusText}. This should never happen` + ); + } - if (!supported) { - throw new Error( - `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.` - ); - } - } + const error_message = await response.text(); + if (error_message) { + throw new Error(error_message); } } diff --git a/packages/kit/src/utils/filesystem.js b/packages/kit/src/utils/filesystem.js index 07c04ba32f7b..1173086bd112 100644 --- a/packages/kit/src/utils/filesystem.js +++ b/packages/kit/src/utils/filesystem.js @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { posixify } from './os.js'; /** @param {string} dir */ export function mkdirp(dir) { @@ -110,11 +111,6 @@ export function walk(cwd, dirs = false) { return (walk_dir(''), all_files); } -/** @param {string} str */ -export function posixify(str) { - return str.replace(/\\/g, '/'); -} - /** * Like `path.join`, but posixified and with a leading `./` if necessary * @param {string[]} str @@ -138,31 +134,6 @@ export function relative_path(from, to) { return join_relative(path.relative(from, to)); } -/** - * Prepend given path with `/@fs` prefix - * @param {string} str - */ -export function to_fs(str) { - str = posixify(str); - return `/@fs${ - // Windows/Linux separation - Windows starts with a drive letter, we need a / in front there - str.startsWith('/') ? '' : '/' - }${str}`; -} - -/** - * Removes `/@fs` prefix from given path and posixifies it - * @param {string} str - */ -export function from_fs(str) { - str = posixify(str); - if (!str.startsWith('/@fs')) return str; - - str = str.slice(4); - // Windows/Linux separation - Windows starts with a drive letter, we need to strip the additional / here - return str[2] === ':' && /[A-Z]/.test(str[1]) ? str.slice(1) : str; -} - /** * Given an entry point like [cwd]/src/hooks, returns a filename like [cwd]/src/hooks.js or [cwd]/src/hooks/index.js * @param {string} entry diff --git a/packages/kit/src/utils/os.js b/packages/kit/src/utils/os.js new file mode 100644 index 000000000000..1101d56fa605 --- /dev/null +++ b/packages/kit/src/utils/os.js @@ -0,0 +1,7 @@ +// this file needs to remain node-agnostic because it could be imported into an +// environment without access to `node:*` + +/** @param {string} str */ +export function posixify(str) { + return str.replace(/\\/g, '/'); +} diff --git a/packages/kit/src/utils/path.js b/packages/kit/src/utils/path.js new file mode 100644 index 000000000000..2ffca61e7f23 --- /dev/null +++ b/packages/kit/src/utils/path.js @@ -0,0 +1,24 @@ +// This file contains Node-agnostic path utilities so that it can be used in +// environments that do not have access to `node:path` (e.g. Cloudflare Workers). +import { posixify } from './os.js'; + +/** + * @param {string} from + * @param {string} to + * @returns {string} + */ +export function relative(from, to) { + const from_parts = from.split('/').filter(Boolean); + const to_parts = to.split('/').filter(Boolean); + let i = 0; + while (i < from_parts.length && i < to_parts.length && from_parts[i] === to_parts[i]) i++; + return [...Array(from_parts.length - i).fill('..'), ...to_parts.slice(i)].join('/') || '.'; +} + +/** + * @param {...string} parts + * @returns {string} + */ +export function join(...parts) { + return parts.map(posixify).join('/'); +} diff --git a/packages/kit/src/utils/path.spec.js b/packages/kit/src/utils/path.spec.js new file mode 100644 index 000000000000..cfadb5a62247 --- /dev/null +++ b/packages/kit/src/utils/path.spec.js @@ -0,0 +1,32 @@ +import { assert, describe } from 'vitest'; +import { relative } from './path.js'; + +describe('relative', (test) => { + test('same path returns .', () => { + assert.equal(relative('/a/b/c', '/a/b/c'), '.'); + }); + + test('sibling path', () => { + assert.equal(relative('/a/b', '/a/c'), '../c'); + }); + + test('child path', () => { + assert.equal(relative('/a', '/a/b/c'), 'b/c'); + }); + + test('parent path', () => { + assert.equal(relative('/a/b/c', '/a'), '../..'); + }); + + test('unrelated paths', () => { + assert.equal(relative('/a/b', '/c/d'), '../../c/d'); + }); + + test('root to child', () => { + assert.equal(relative('/', '/a/b'), 'a/b'); + }); + + test('child to root', () => { + assert.equal(relative('/a/b', '/'), '../..'); + }); +}); diff --git a/packages/kit/test/apps/basics/svelte.config.js b/packages/kit/test/apps/basics/svelte.config.js index c6fea0f8b05f..7e201e39ed83 100644 --- a/packages/kit/test/apps/basics/svelte.config.js +++ b/packages/kit/test/apps/basics/svelte.config.js @@ -3,30 +3,6 @@ import process from 'node:process'; /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: { - name: 'test-adapter', - adapt(builder) { - builder.instrument({ - entrypoint: `${builder.getServerDirectory()}/index.js`, - instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`, - module: { - exports: ['Server'] - } - }); - }, - emulate() { - return { - platform({ config, prerender }) { - return { config, prerender }; - } - }; - }, - supports: { - read: () => true, - instrumentation: () => true - } - }, - experimental: { remoteFunctions: true, tracing: { diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 768a69f8039d..7f26e92f44cb 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -8,32 +8,6 @@ test.skip(() => process.env.KIT_E2E_BROWSER === 'webkit'); test.describe.configure({ mode: 'parallel' }); -test.describe('adapter', () => { - test('populates event.platform for dynamic SSR', async ({ page }) => { - await page.goto('/adapter/dynamic'); - const json = JSON.parse((await page.textContent('pre')) ?? ''); - - expect(json).toEqual({ - config: { - message: 'hello from dynamic page' - }, - prerender: false - }); - }); - - test('populates event.platform for prerendered page', async ({ page }) => { - await page.goto('/adapter/prerendered'); - const json = JSON.parse((await page.textContent('pre')) ?? ''); - - expect(json).toEqual({ - config: { - message: 'hello from prerendered page' - }, - prerender: true - }); - }); -}); - test.describe('Imports', () => { // https://github.com/sveltejs/kit/issues/461 test('handles static asset imports', async ({ baseURL, page }) => { diff --git a/packages/kit/test/apps/basics/vite.config.js b/packages/kit/test/apps/basics/vite.config.js index 415957fac6da..d61ad0f73f94 100644 --- a/packages/kit/test/apps/basics/vite.config.js +++ b/packages/kit/test/apps/basics/vite.config.js @@ -13,7 +13,26 @@ export default defineConfig({ // the reload confuses Playwright include: ['cookie'] }, - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: { + name: 'test-adapter', + adapt(builder) { + builder.instrument({ + entrypoint: `${builder.getServerDirectory()}/index.js`, + instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`, + module: { + exports: ['Server'] + } + }); + }, + supports: { + read: () => true, + instrumentation: () => true + } + } + }) + ], server: { fs: { allow: [path.resolve('../../../src')] diff --git a/packages/kit/test/apps/dev-only/test/test.js b/packages/kit/test/apps/dev-only/test/test.js index 6e2a96dd8782..aeaeacea11f5 100644 --- a/packages/kit/test/apps/dev-only/test/test.js +++ b/packages/kit/test/apps/dev-only/test/test.js @@ -105,7 +105,7 @@ test.describe('Vite', () => { expect(manifest).toHaveProperty('optimized.e2e-test-dep-page-universal'); }); - test('skips optimizing +page.server.js dependencies', async ({ page }) => { + test('optimizes +page.server.js dependencies', async ({ page }) => { await page.goto('/'); await page.getByText('hello world!').waitFor(); @@ -115,7 +115,7 @@ test.describe('Vite', () => { ); const manifest = JSON.parse(fs.readFileSync(manifest_path, 'utf-8')); - expect(manifest).not.toHaveProperty('optimized.e2e-test-dep-page-server'); + expect(manifest).toHaveProperty('optimized.e2e-test-dep-page-server'); }); test('optimizes +layout.svelte dependencies', async ({ page }) => { @@ -144,7 +144,7 @@ test.describe('Vite', () => { expect(manifest).toHaveProperty('optimized.e2e-test-dep-layout-universal'); }); - test('skips optimizing +layout.server.js dependencies', async ({ page }) => { + test('optimizes +layout.server.js dependencies', async ({ page }) => { await page.goto('/'); await page.getByText('hello world!').waitFor(); @@ -154,7 +154,7 @@ test.describe('Vite', () => { ); const manifest = JSON.parse(fs.readFileSync(manifest_path, 'utf-8')); - expect(manifest).not.toHaveProperty('optimized.e2e-test-dep-layout-server'); + expect(manifest).toHaveProperty('optimized.e2e-test-dep-layout-server'); }); test('optimizes +error.svelte dependencies', async ({ page }) => { diff --git a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/svelte.config.js b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/svelte.config.js index 6e60dbbbdd9c..821c14379ec8 100644 --- a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/svelte.config.js +++ b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../../../adapter-auto/index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter() - } + kit: {} }; export default config; diff --git a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/vite.config.js b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/vite.config.js index 3f12d3677ea6..0a6fb79b39ef 100644 --- a/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/vite.config.js +++ b/packages/kit/test/build-errors/apps/prerender-entry-generator-mismatch/vite.config.js @@ -1,5 +1,6 @@ import * as path from 'node:path'; import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../../../adapter-auto/index.js'; /** @type {import('vite').UserConfig} */ const config = { @@ -11,7 +12,11 @@ const config = { logLevel: 'silent', - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: adapter() + }) + ], server: { fs: { diff --git a/packages/kit/test/build-errors/apps/prerender-remote-function-error/svelte.config.js b/packages/kit/test/build-errors/apps/prerender-remote-function-error/svelte.config.js index f9e8b0b2b482..d50ca84160f4 100644 --- a/packages/kit/test/build-errors/apps/prerender-remote-function-error/svelte.config.js +++ b/packages/kit/test/build-errors/apps/prerender-remote-function-error/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../../../adapter-auto/index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter(), - experimental: { remoteFunctions: true } diff --git a/packages/kit/test/build-errors/apps/prerender-remote-function-error/vite.config.js b/packages/kit/test/build-errors/apps/prerender-remote-function-error/vite.config.js index 3f12d3677ea6..0a6fb79b39ef 100644 --- a/packages/kit/test/build-errors/apps/prerender-remote-function-error/vite.config.js +++ b/packages/kit/test/build-errors/apps/prerender-remote-function-error/vite.config.js @@ -1,5 +1,6 @@ import * as path from 'node:path'; import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../../../adapter-auto/index.js'; /** @type {import('vite').UserConfig} */ const config = { @@ -11,7 +12,11 @@ const config = { logLevel: 'silent', - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: adapter() + }) + ], server: { fs: { diff --git a/packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/svelte.config.js b/packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/svelte.config.js index 6e60dbbbdd9c..821c14379ec8 100644 --- a/packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/svelte.config.js +++ b/packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../../../adapter-auto/index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter() - } + kit: {} }; export default config; diff --git a/packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/vite.config.js b/packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/vite.config.js index 3f12d3677ea6..0a6fb79b39ef 100644 --- a/packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/vite.config.js +++ b/packages/kit/test/build-errors/apps/prerenderable-incorrect-fragment/vite.config.js @@ -1,5 +1,6 @@ import * as path from 'node:path'; import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../../../adapter-auto/index.js'; /** @type {import('vite').UserConfig} */ const config = { @@ -11,7 +12,11 @@ const config = { logLevel: 'silent', - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: adapter() + }) + ], server: { fs: { diff --git a/packages/kit/test/build-errors/apps/prerenderable-not-prerendered/svelte.config.js b/packages/kit/test/build-errors/apps/prerenderable-not-prerendered/svelte.config.js index 6e60dbbbdd9c..821c14379ec8 100644 --- a/packages/kit/test/build-errors/apps/prerenderable-not-prerendered/svelte.config.js +++ b/packages/kit/test/build-errors/apps/prerenderable-not-prerendered/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../../../adapter-auto/index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { - kit: { - adapter: adapter() - } + kit: {} }; export default config; diff --git a/packages/kit/test/build-errors/apps/prerenderable-not-prerendered/vite.config.js b/packages/kit/test/build-errors/apps/prerenderable-not-prerendered/vite.config.js index 3f12d3677ea6..0a6fb79b39ef 100644 --- a/packages/kit/test/build-errors/apps/prerenderable-not-prerendered/vite.config.js +++ b/packages/kit/test/build-errors/apps/prerenderable-not-prerendered/vite.config.js @@ -1,5 +1,6 @@ import * as path from 'node:path'; import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../../../adapter-auto/index.js'; /** @type {import('vite').UserConfig} */ const config = { @@ -11,7 +12,11 @@ const config = { logLevel: 'silent', - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: adapter() + }) + ], server: { fs: { diff --git a/packages/kit/test/build-errors/prerender.spec.js b/packages/kit/test/build-errors/prerender.spec.js index e788055bac8b..09be3d111de6 100644 --- a/packages/kit/test/build-errors/prerender.spec.js +++ b/packages/kit/test/build-errors/prerender.spec.js @@ -1,7 +1,6 @@ import { assert, test } from 'vitest'; import { execSync } from 'node:child_process'; import path from 'node:path'; -import { EOL } from 'node:os'; const timeout = 60_000; @@ -25,7 +24,7 @@ test('entry generators should match their own route', { timeout }, () => { stdio: 'pipe', timeout }), - `Error: The entries export from /[slug]/[notSpecific] generated entry /whatever/specific, which was matched by /[slug]/specific - see the \`handleEntryGeneratorMismatch\` option in https://svelte.dev/docs/kit/configuration#prerender for more info.${EOL}To suppress or handle this error, implement \`handleEntryGeneratorMismatch\` in https://svelte.dev/docs/kit/configuration#prerender` + /Error: The entries export from \/\[slug\]\/\[notSpecific\] generated entry \/whatever\/specific, which was matched by \/\[slug\]\/specific - see the `handleEntryGeneratorMismatch` option in https:\/\/svelte\.dev\/docs\/kit\/configuration#prerender for more info\.\r?\nTo suppress or handle this error, implement `handleEntryGeneratorMismatch` in https:\/\/svelte\.dev\/docs\/kit\/configuration#prerender/ ); }); diff --git a/packages/kit/test/prerendering/basics/svelte.config.js b/packages/kit/test/prerendering/basics/svelte.config.js index da247d1b0ab9..f75006c4b8bc 100644 --- a/packages/kit/test/prerendering/basics/svelte.config.js +++ b/packages/kit/test/prerendering/basics/svelte.config.js @@ -1,11 +1,8 @@ -import adapter from '../../../../adapter-static/index.js'; import { writeFileSync } from 'node:fs'; /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter(), - prerender: { handleHttpError: 'warn', origin: 'http://prerender.origin', diff --git a/packages/kit/test/prerendering/basics/vite.config.js b/packages/kit/test/prerendering/basics/vite.config.js index 253a50dc5b65..5037f8784efe 100644 --- a/packages/kit/test/prerendering/basics/vite.config.js +++ b/packages/kit/test/prerendering/basics/vite.config.js @@ -1,5 +1,6 @@ import * as path from 'node:path'; import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../../adapter-static/index.js'; /** @type {import('vitest/config').ViteUserConfig} */ const config = { @@ -11,7 +12,11 @@ const config = { logLevel: 'silent', - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: adapter() + }) + ], define: { 'process.env.MY_ENV': '"MY_ENV DEFINED"' diff --git a/packages/kit/test/prerendering/options/svelte.config.js b/packages/kit/test/prerendering/options/svelte.config.js index 4117f748ad4e..e336ef98f99d 100644 --- a/packages/kit/test/prerendering/options/svelte.config.js +++ b/packages/kit/test/prerendering/options/svelte.config.js @@ -1,5 +1,3 @@ -import adapter from '../../../../adapter-static/index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { compilerOptions: { @@ -9,10 +7,6 @@ const config = { }, kit: { - adapter: adapter({ - fallback: '200.html' - }), - csp: { directives: { 'script-src': ['self'] diff --git a/packages/kit/test/prerendering/options/vite.config.js b/packages/kit/test/prerendering/options/vite.config.js index 4b90a7655ee5..5f7960359333 100644 --- a/packages/kit/test/prerendering/options/vite.config.js +++ b/packages/kit/test/prerendering/options/vite.config.js @@ -1,5 +1,6 @@ import * as path from 'node:path'; import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../../adapter-static/index.js'; /** @type {import('vite').UserConfig} */ const config = { @@ -11,7 +12,13 @@ const config = { logLevel: 'silent', - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: adapter({ + fallback: '200.html' + }) + }) + ], server: { fs: { diff --git a/packages/kit/test/prerendering/paths-base/svelte.config.js b/packages/kit/test/prerendering/paths-base/svelte.config.js index 27e3e4661b69..71cf6f393a37 100644 --- a/packages/kit/test/prerendering/paths-base/svelte.config.js +++ b/packages/kit/test/prerendering/paths-base/svelte.config.js @@ -1,10 +1,6 @@ -import adapter from '../../../../adapter-static/index.js'; - /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter(), - paths: { base: '/path-base', relative: false diff --git a/packages/kit/test/prerendering/paths-base/vite.config.js b/packages/kit/test/prerendering/paths-base/vite.config.js index 2857b2592d11..b03a03e20913 100644 --- a/packages/kit/test/prerendering/paths-base/vite.config.js +++ b/packages/kit/test/prerendering/paths-base/vite.config.js @@ -1,5 +1,6 @@ import * as path from 'node:path'; import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../../adapter-static/index.js'; /** @type {import('vite').UserConfig} */ const config = { @@ -13,7 +14,11 @@ const config = { logLevel: 'silent', - plugins: [sveltekit()], + plugins: [ + sveltekit({ + adapter: adapter() + }) + ], server: { fs: { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 1e14b713aa1b..e74da84c0398 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -4,6 +4,7 @@ declare module '@sveltejs/kit' { import type { SvelteConfig } from '@sveltejs/vite-plugin-svelte'; import type { StandardSchemaV1 } from '@standard-schema/spec'; + import type { PluginOption } from 'vite'; import type { RouteId as AppRouteId, LayoutParams as AppLayoutParams, ResolvedPathname } from '$app/types'; // @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; @@ -37,11 +38,14 @@ declare module '@sveltejs/kit' { */ instrumentation?: () => boolean; }; - /** - * Creates an `Emulator`, which allows the adapter to influence the environment - * during dev, build and prerendering. - */ - emulate?: () => MaybePromise; + vite?: { + /** + * Add a Vite plugin here to replace the default Node SSR environment. + * The provided Vite plugins should configure the dev and preview servers + * @since 3.0.0 + */ + plugins?: PluginOption; + }; } export type LoadProperties | void> = input extends void @@ -78,6 +82,12 @@ declare module '@sveltejs/kit' { ? undefined // needs to be undefined, because void will corrupt union type : T; + export interface ManifestGenerationOptions { + /** A relative path to the base directory of the server build output */ + relativePath: string; + routes?: RouteDefinition[]; + } + /** * This object is passed to the `adapt` function of adapters. * It contains various methods and properties that are useful for adapting the app. @@ -121,9 +131,8 @@ 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 a relative path to the base directory of the app and optionally in which format (esm or cjs) the manifest should be generated */ - generateManifest: (opts: { relativePath: string; routes?: RouteDefinition[] }) => string; + generateManifest: (opts: ManifestGenerationOptions) => string; /** * Resolve a path to the `name` directory inside `outDir`, e.g. `/path/to/.svelte-kit/my-adapter`. @@ -283,21 +292,12 @@ declare module '@sveltejs/kit' { 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 { + // TODO: remove this in 4.0 /** * Your [adapter](https://svelte.dev/docs/kit/adapters) is run when executing `vite build`. It determines how the output is converted for different platforms. * @default undefined + * @deprecated removed in 3.0.0. Adapters should now be passed to the `sveltekit` Vite plugin in `vite.config.js` */ adapter?: Adapter; /** @@ -883,6 +883,15 @@ declare module '@sveltejs/kit' { }; } + export interface KitViteConfig { + /** + * Your [adapter](https://svelte.dev/docs/kit/adapters) is run when executing `vite build`. It determines how the output is converted for different platforms. + * @since 3.0.0 + * @default undefined + */ + adapter?: Adapter; + } + /** * The [`handle`](https://svelte.dev/docs/kit/hooks#Server-hooks-handle) hook runs every time the SvelteKit server receives a [request](https://svelte.dev/docs/kit/web-standards#Fetch-APIs-Request) and * determines the [response](https://svelte.dev/docs/kit/web-standards#Fetch-APIs-Response). @@ -1619,19 +1628,25 @@ declare module '@sveltejs/kit' { read?: (file: string) => MaybePromise; } + /** + * Required to instantiate `Server` with project specific information + */ 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; + base: string; /** Static files from `kit.config.files.assets` and the service worker (if any). */ assets: Set; mimeTypes: Record; - /** private fields */ + /** @internal private fields */ _: { client: NonNullable; nodes: SSRNodeLoader[]; /** hashed filename -> import to that file */ - remotes: Record Promise>; + remotes: Record Promise<{ default: Record }>>; routes: SSRRoute[]; prerendered_routes: Set; matchers: () => Promise>; @@ -2508,8 +2523,11 @@ declare module '@sveltejs/kit' { } 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; @@ -2550,6 +2568,9 @@ declare module '@sveltejs/kit' { server_manifest: import('vite').Manifest; } + /** + * Used to construct the SSR manifest + */ interface ManifestData { /** Static files from `kit.config.files.assets`. */ assets: Asset[]; @@ -2736,9 +2757,8 @@ declare module '@sveltejs/kit' { extensions: string[]; }; - type ValidatedKitConfig = Omit, 'adapter'> & { - adapter?: Adapter; - }; + // TODO: remove the omit in 4.0 + type ValidatedKitConfig = Omit, 'adapter'>; /** * Throws an error with a HTTP status code and an optional message. * When called during request handling, this will cause SvelteKit to @@ -2977,11 +2997,12 @@ declare module '@sveltejs/kit/node' { } declare module '@sveltejs/kit/vite' { - import type { Plugin } from 'vite'; + import type { KitViteConfig } from '@sveltejs/kit'; + import type { PluginOption } from 'vite'; /** * Returns the SvelteKit Vite plugins. * */ - export function sveltekit(): Promise; + export function sveltekit(config?: KitViteConfig): Promise; export {}; } @@ -3671,6 +3692,7 @@ declare namespace App { /** * The interface that defines `event.locals`, which can be accessed in server [hooks](https://svelte.dev/docs/kit/hooks) (`handle`, and `handleError`), server-only `load` functions, and `+server.js` files. */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface Locals {} /** @@ -3678,16 +3700,19 @@ declare namespace App { * The `Load` and `ServerLoad` functions in `./$types` will be narrowed accordingly. * Use optional properties for data that is only present on specific pages. Do not add an index signature (`[key: string]: any`). */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface PageData {} /** * The shape of the `page.state` object, which can be manipulated using the [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) functions from `$app/navigation`. */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface PageState {} /** * If your adapter provides [platform-specific context](https://svelte.dev/docs/kit/adapters#Platform-specific-context) via `event.platform`, you can specify it here. */ + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface Platform {} } @@ -3787,4 +3812,57 @@ 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; +} + //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/playgrounds/basic/svelte.config.js b/playgrounds/basic/svelte.config.js index 28802f1064e9..d5098a971446 100644 --- a/playgrounds/basic/svelte.config.js +++ b/playgrounds/basic/svelte.config.js @@ -1,5 +1,3 @@ -import adapter from '@sveltejs/adapter-node'; - /** @type {import('@sveltejs/kit').Config} */ const config = { compilerOptions: { @@ -9,7 +7,6 @@ const config = { }, kit: { - adapter: adapter(), experimental: { remoteFunctions: true } diff --git a/playgrounds/basic/vite.config.js b/playgrounds/basic/vite.config.js index e9e130ab87a3..2611ae699ea3 100644 --- a/playgrounds/basic/vite.config.js +++ b/playgrounds/basic/vite.config.js @@ -1,8 +1,9 @@ +import adapter from '@sveltejs/adapter-node'; import { enhancedImages } from '@sveltejs/enhanced-img'; import { sveltekit } from '@sveltejs/kit/vite'; export default { - plugins: [enhancedImages(), sveltekit()], + plugins: [enhancedImages(), sveltekit({ adapter: adapter() })], server: { fs: { allow: ['../../packages/kit']