Repository: sveltejs/kit
Pull request #6562 (2022-09-04 12:08:20)

Update pnpm to v7.11.0
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [pnpm](https://pnpm.io) ([source](https://togithub.com/pnpm/pnpm)) | [`7.9.5` -> `7.11.0`](https://renovatebot.com/diffs/npm/pnpm/7.9.5/7.11.0) | [![age](https://badges.renovateapi.com/packages/npm/pnpm/7.11.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/pnpm/7.11.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/pnpm/7.11.0/compatibility-slim/7.9.5)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/pnpm/7.11.0/confidence-slim/7.9.5)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>pnpm/pnpm</summary>

### [`v7.11.0`](https://togithub.com/pnpm/pnpm/releases/tag/v7.11.0)

[Compare Source](https://togithub.com/pnpm/pnpm/compare/v7.10.0...v7.11.0)

#### Minor Changes

-   `pnpm patch`: edit the patched package in a directory specified by the `--edit-dir` option. E.g., `pnpm patch express@3.1.0 --edit-dir=/home/xxx/src/patched-express` [#&#8203;5304](https://togithub.com/pnpm/pnpm/pull/5304)

#### Patch Changes

-   Auto installing a peer dependency in a workspace that also has it as a dev dependency in another project [#&#8203;5144](https://togithub.com/pnpm/pnpm/issues/5144).
-   When an error happens during installation of a subdependency, print some context information in order to be able to locate that subdependency. Print the exact chain of packages that led to the problematic dependency.

#### Our Gold Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
        <a href="https://bit.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80"></a>
      </td>
      <td align="center" valign="middle">
        <a href="https://nhost.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/nhost.svg" width="180"></a>
      </td>
      <td align="center" valign="middle">
        <a href="https://novu.co/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/novu.svg" width="180"></a>
      </td>
    </tr>
  </tbody>
</table>

#### Our Silver Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
        <a href="https://prisma.io/?utm_source=pnpm&utm_medium=readme" target="_blank">
          <img src="https://pnpm.io/img/users/prisma.svg" width="180">
        </a>
      </td>
      <td align="center" valign="middle">
        <a href="https://leniolabs.com/?utm_source=pnpm&utm_medium=readme" target="_blank">
          <img src="https://pnpm.io/img/users/leniolabs.jpg" width="80">
        </a>
      </td>
      <td align="center" valign="middle">
        <a href="https://vercel.com/?utm_source=pnpm&utm_medium=readme" target="_blank">
          <img src="https://pnpm.io/img/users/vercel.svg" width="180">
        </a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
        <a href="https://www.takeshape.io/?utm_source=pnpm&utm_medium=readme" target="_blank">
          <img src="https://pnpm.io/img/users/takeshape.svg" width="280">
        </a>
      </td>
      <td align="center" valign="middle">
        <a href="https://doppler.com/?utm_source=pnpm&utm_medium=readme#gh-light-mode-only" target="_blank">
          <img src="https://pnpm.io/img/users/doppler.svg" width="280">
        </a>
      </td>
    </tr>
  </tbody>
</table>

#### What's Changed
* feat(plugin-command-patching): add path option to patch command by @&#8203;roseline1[https://github.com/pnpm/pnpm/pull/5304](https://togithub.com/pnpm/pnpm/pull/5304)l/5304
* fix: improve dependency installation error message by @&#8203;zkoch[https://github.com/pnpm/pnpm/pull/5247](https://togithub.com/pnpm/pnpm/pull/5247)l/5247
* fix: auto installing peer dep in a workspace by @&#8203;zkoch[https://github.com/pnpm/pnpm/pull/5307](https://togithub.com/pnpm/pnpm/pull/5307)l/5307

#### New Contributors
* @&#8203;roseline124 made their first contributi[https://github.com/pnpm/pnpm/pull/5304](https://togithub.com/pnpm/pnpm/pull/5304)l/5304

**Full Changelog**: https://github.com/pnpm/pnpm/compare/v7.10.0...v7.11.0

### [`v7.10.0`](https://togithub.com/pnpm/pnpm/releases/tag/v7.10.0)

[Compare Source](https://togithub.com/pnpm/pnpm/compare/v7.9.5...v7.10.0)

#### Minor Changes

-   New time-based resolution strategy supported.

    When `resolution-mode` is set to `time-based`, pnpm will resolve dependencies the following way:

    1.  Direct dependencies will be resolved to their lowest versions. So if there is `foo@^1.1.0` in the dependencies, then `1.1.0` will be installed.
    2.  Subdependencies will be resolved from versions that were published before the last direct dependency was published.

    With this resolution mode installations with hot cache are faster. It also reduces the chance of subdependency hijacking as subdependencies will be updated only if direct dependencies are updated.

    This resolution mode works only with npm's [full metadata](https://togithub.com/npm/registry/blob/master/docs/responses/package-metadata.md#full-metadata-format). So it is slower in some scenarios. However, if you use [Verdaccio](https://verdaccio.org/) v5.15.1 or newer, you may set the `registry-supports-time-field` setting to `true`, and it will be really fast.

    Related [RFC](https://togithub.com/pnpm/rfcs/pull/2).

-   Enhance `pnpm env` with the `remove` command. To remove a Node.js version installed by pnpm, run:

        pnpm env remove --global <node.js version>

#### Patch Changes

-   `pnpm store prune` should remove all cached metadata.
-   Don't modify the manifest of the injected workspace project, when it has the same dependency in prod and peer dependencies.

#### Our Gold Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
        <a href="https://bit.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80"></a>
      </td>
      <td align="center" valign="middle">
        <a href="https://nhost.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/nhost.svg" width="180"></a>
      </td>
      <td align="center" valign="middle">
        <a href="https://novu.co/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/novu.svg" width="180"></a>
      </td>
    </tr>
  </tbody>
</table>

#### Our Silver Sponsors

<table>
  <tbody>
    <tr>
      <td align="center" valign="middle">
        <a href="https://prisma.io/?utm_source=pnpm&utm_medium=readme" target="_blank">
          <img src="https://pnpm.io/img/users/prisma.svg" width="180">
        </a>
      </td>
      <td align="center" valign="middle">
        <a href="https://leniolabs.com/?utm_source=pnpm&utm_medium=readme" target="_blank">
          <img src="https://pnpm.io/img/users/leniolabs.jpg" width="80">
        </a>
      </td>
      <td align="center" valign="middle">
        <a href="https://vercel.com/?utm_source=pnpm&utm_medium=readme" target="_blank">
          <img src="https://pnpm.io/img/users/vercel.svg" width="180">
        </a>
      </td>
    </tr>
    <tr>
      <td align="center" valign="middle">
        <a href="https://www.takeshape.io/?utm_source=pnpm&utm_medium=readme" target="_blank">
          <img src="https://pnpm.io/img/users/takeshape.svg" width="280">
        </a>
      </td>
      <td align="center" valign="middle">
        <a href="https://doppler.com/?utm_source=pnpm&utm_medium=readme#gh-light-mode-only" target="_blank">
          <img src="https://pnpm.io/img/users/doppler.svg" width="280">
        </a>
      </td>
    </tr>
  </tbody>
</table>

#### What's Changed
* test: fix NODE_PATH test on Windows by @&#8203;zkoch[https://github.com/pnpm/pnpm/pull/5257](https://togithub.com/pnpm/pnpm/pull/5257)l/5257
* refactor: resolve dependencies by @&#8203;zkoch[https://github.com/pnpm/pnpm/pull/5267](https://togithub.com/pnpm/pnpm/pull/5267)l/5267
* feat(env): add remove command to pnpm env by @&#8203;mark-omar[https://github.com/pnpm/pnpm/pull/5263](https://togithub.com/pnpm/pnpm/pull/5263)l/5263
* docs(CONTRIBUTING): correct setup instructions for first-time clone by @&#8203;gwhitn[https://github.com/pnpm/pnpm/pull/5289](https://togithub.com/pnpm/pnpm/pull/5289)l/5289
* fix: don't modify the injected project's manifest by @&#8203;zkoch[https://github.com/pnpm/pnpm/pull/5294](https://togithub.com/pnpm/pnpm/pull/5294)l/5294
* feat: time-based resolution mode by @&#8203;zkoch[https://github.com/pnpm/pnpm/pull/5238](https://togithub.com/pnpm/pnpm/pull/5238)l/5238
* feat: resolve direct deps to lowest version when resolution-mode is time-based by @&#8203;zkoch[https://github.com/pnpm/pnpm/pull/5298](https://togithub.com/pnpm/pnpm/pull/5298)l/5298
* fix: add fallback for time-based resolution by @&#8203;zkoch[https://github.com/pnpm/pnpm/pull/5302](https://togithub.com/pnpm/pnpm/pull/5302)l/5302

#### New Contributors
* @&#8203;mark-omarov made their first contributi[https://github.com/pnpm/pnpm/pull/5263](https://togithub.com/pnpm/pnpm/pull/5263)l/5263
* @&#8203;gwhitney made their first contributi[https://github.com/pnpm/pnpm/pull/5289](https://togithub.com/pnpm/pnpm/pull/5289)l/5289

**Full Changelog**: https://github.com/pnpm/pnpm/compare/v7.9.5...v7.10.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/sveltejs/kit).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzMi4xODkuMCIsInVwZGF0ZWRJblZlciI6IjMyLjE5NC4yIn0=-->


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: e4ec128694c6058bb75aef48549f90dda55bdf7e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/sveltejs/kit/new/renovate/all-minor-patch?filename=.changeset/tiny-flowers-exercise.md&value=---%0A%0A---%0A%0AUpdate%20pnpm%20to%20v7.10.0%0A)


netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *kit-demo* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | e4ec128694c6058bb75aef48549f90dda55bdf7e |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kit-demo/deploys/63149535f88f3d00097707bb |

Patch:
diff --git a/.changeset/blue-glasses-compete.md b/.changeset/blue-glasses-compete.md
new file mode 100644
index 000000000..48fdd5011
--- /dev/null
+++ b/.changeset/blue-glasses-compete.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] ActionData type
diff --git a/.changeset/eight-vans-exercise.md b/.changeset/eight-vans-exercise.md
new file mode 100644
index 000000000..ac68d48d8
--- /dev/null
+++ b/.changeset/eight-vans-exercise.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-auto': patch
+---
+
+[feat] support Azure SWA
diff --git a/.changeset/fair-rivers-repair.md b/.changeset/fair-rivers-repair.md
new file mode 100644
index 000000000..186008088
--- /dev/null
+++ b/.changeset/fair-rivers-repair.md
@@ -0,0 +1,5 @@
+---
+"@sveltejs/adapter-node": patch
+---
+
+Allow streaming when `BODY_SIZE_LIMIT` is set
diff --git a/.changeset/friendly-starfishes-yell.md b/.changeset/friendly-starfishes-yell.md
new file mode 100644
index 000000000..f68ed9672
--- /dev/null
+++ b/.changeset/friendly-starfishes-yell.md
@@ -0,0 +1,9 @@
+---
+'@sveltejs/adapter-cloudflare': patch
+'@sveltejs/adapter-cloudflare-workers': patch
+'@sveltejs/adapter-netlify': patch
+'@sveltejs/adapter-node': patch
+'@sveltejs/adapter-vercel': patch
+---
+
+Update to esbuild 0.15
diff --git a/.changeset/gold-walls-rush.md b/.changeset/gold-walls-rush.md
new file mode 100644
index 000000000..f8b7a8951
--- /dev/null
+++ b/.changeset/gold-walls-rush.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Change illegal import message to reference public-facing code rather than client-side code
diff --git a/.changeset/long-icons-hope.md b/.changeset/long-icons-hope.md
new file mode 100644
index 000000000..59f471c25
--- /dev/null
+++ b/.changeset/long-icons-hope.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] hooks file renames; error shape defined through handleError
diff --git a/.changeset/lovely-sloths-flow.md b/.changeset/lovely-sloths-flow.md
new file mode 100644
index 000000000..c2cfa9d81
--- /dev/null
+++ b/.changeset/lovely-sloths-flow.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[chore] Refactor graph analysis for better unit tests
diff --git a/.changeset/modern-books-confess.md b/.changeset/modern-books-confess.md
new file mode 100644
index 000000000..c46179a1f
--- /dev/null
+++ b/.changeset/modern-books-confess.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] error when using `paths.base` and `import` assets
diff --git a/.changeset/modern-falcons-tan.md b/.changeset/modern-falcons-tan.md
new file mode 100644
index 000000000..a3649d2e2
--- /dev/null
+++ b/.changeset/modern-falcons-tan.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+[breaking] update use:enhance usage
diff --git a/.changeset/nasty-hornets-sneeze.md b/.changeset/nasty-hornets-sneeze.md
new file mode 100644
index 000000000..61eaa37c0
--- /dev/null
+++ b/.changeset/nasty-hornets-sneeze.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-netlify': patch
+---
+
+[feat] only generate ESM functions
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 5b84073c1..970fe858b 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -349,6 +349,7 @@
     "friendly-dodos-exercise",
     "friendly-eggs-cry",
     "friendly-rivers-argue",
+    "friendly-starfishes-yell",
     "funny-ads-move",
     "funny-keys-appear",
     "funny-needles-talk",
@@ -557,6 +558,7 @@
     "long-bulldogs-invent",
     "long-guests-trade",
     "long-hotels-hunt",
+    "long-icons-hope",
     "long-moles-fold",
     "long-turkeys-boil",
     "loud-dancers-cough",
@@ -620,6 +622,7 @@
     "modern-cameras-fetch",
     "modern-donkeys-invite",
     "modern-dryers-join",
+    "modern-falcons-tan",
     "modern-files-watch",
     "modern-grapes-tell",
     "modern-lies-deny",
@@ -642,6 +645,7 @@
     "nasty-boats-bathe",
     "nasty-buttons-invent",
     "nasty-falcons-ring",
+    "nasty-hornets-sneeze",
     "nasty-lemons-provide",
     "nasty-llamas-lay",
     "nasty-seahorses-know",
@@ -846,6 +850,7 @@
     "real-pens-watch",
     "real-trainers-collect",
     "red-adults-wonder",
+    "red-cherries-fly",
     "red-cooks-fix",
     "red-masks-confess",
     "red-papayas-prove",
@@ -1076,6 +1081,7 @@
     "stupid-bottles-approve",
     "stupid-countries-sneeze",
     "stupid-dolls-fly",
+    "stupid-eyes-attend",
     "stupid-laws-jog",
     "stupid-maps-hide",
     "stupid-owls-provide",
@@ -1110,6 +1116,7 @@
     "tame-rings-cry",
     "tasty-cars-love",
     "tasty-donkeys-wait",
+    "tasty-geese-burn",
     "tasty-geese-hug",
     "tasty-islands-drive",
     "tasty-squids-share",
@@ -1170,6 +1177,7 @@
     "tiny-gorillas-whisper",
     "tiny-horses-rush",
     "tiny-hounds-glow",
+    "tiny-houses-matter",
     "tiny-humans-yawn",
     "tiny-kids-sort",
     "tiny-moles-joke",
@@ -1191,6 +1199,7 @@
     "tricky-bees-yell",
     "tricky-buses-punch",
     "tricky-cheetahs-warn",
+    "tricky-ducks-kneel",
     "tricky-maps-know",
     "tricky-moose-sip",
     "tricky-news-smash",
@@ -1203,9 +1212,11 @@
     "twelve-goats-knock",
     "twelve-olives-confess",
     "twelve-onions-burn",
+    "twelve-points-knock",
     "twelve-snakes-tan",
     "twelve-stingrays-cry",
     "twenty-ads-turn",
+    "twenty-apricots-grin",
     "twenty-carpets-enjoy",
     "twenty-dogs-shout",
     "twenty-dryers-hope",
@@ -1244,6 +1255,7 @@
     "violet-mugs-brake",
     "violet-spies-play",
     "violet-walls-jam",
+    "violet-wolves-reflect",
     "warm-ads-invite",
     "warm-cameras-fix",
     "warm-cheetahs-wash",
diff --git a/.changeset/red-cherries-fly.md b/.changeset/red-cherries-fly.md
new file mode 100644
index 000000000..09d977507
--- /dev/null
+++ b/.changeset/red-cherries-fly.md
@@ -0,0 +1,7 @@
+---
+'@sveltejs/adapter-node': patch
+'@sveltejs/adapter-vercel': patch
+'@sveltejs/kit': patch
+---
+
+[breaking] request creation cleanup
diff --git a/.changeset/stupid-eyes-attend.md b/.changeset/stupid-eyes-attend.md
new file mode 100644
index 000000000..38eb82987
--- /dev/null
+++ b/.changeset/stupid-eyes-attend.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+SvelteKit 1.0 RC
diff --git a/.changeset/tasty-geese-burn.md b/.changeset/tasty-geese-burn.md
new file mode 100644
index 000000000..e7834786d
--- /dev/null
+++ b/.changeset/tasty-geese-burn.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] docs links in generated and published types
diff --git a/.changeset/tiny-houses-matter.md b/.changeset/tiny-houses-matter.md
new file mode 100644
index 000000000..dd71c5842
--- /dev/null
+++ b/.changeset/tiny-houses-matter.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] make action passed to enhance function a URL instead of a string
diff --git a/.changeset/tricky-ducks-kneel.md b/.changeset/tricky-ducks-kneel.md
new file mode 100644
index 000000000..9616e9e54
--- /dev/null
+++ b/.changeset/tricky-ducks-kneel.md
@@ -0,0 +1,6 @@
+---
+'@sveltejs/adapter-node': patch
+'@sveltejs/kit': patch
+---
+
+[breaking] limit adapter-node request size
diff --git a/.changeset/twelve-points-knock.md b/.changeset/twelve-points-knock.md
new file mode 100644
index 000000000..b528e2e8b
--- /dev/null
+++ b/.changeset/twelve-points-knock.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+[breaking] hooks file rename
diff --git a/.changeset/twenty-apricots-grin.md b/.changeset/twenty-apricots-grin.md
new file mode 100644
index 000000000..b80319f65
--- /dev/null
+++ b/.changeset/twenty-apricots-grin.md
@@ -0,0 +1,6 @@
+---
+"@sveltejs/adapter-node": patch
+"@sveltejs/adapter-static": patch
+---
+
+Improve performance by compressing in parallel
diff --git a/.changeset/violet-wolves-reflect.md b/.changeset/violet-wolves-reflect.md
new file mode 100644
index 000000000..d12f71b28
--- /dev/null
+++ b/.changeset/violet-wolves-reflect.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] remove element property; enhance can only be used on form elements
diff --git a/README.md b/README.md
index 795f66690..fa0f346db 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,14 @@
 
 # READ THIS FIRST!
 
-SvelteKit is still in beta. Expect bugs! Read more [here](https://svelte.dev/blog/sveltekit-beta), and track progress towards 1.0 [here](https://github.com/sveltejs/kit/issues?q=is%3Aopen+is%3Aissue+milestone%3A1.0).
+SvelteKit is in its release candidate phase:
+- :exclamation: expect bugs! 
+- :heavy_check_mark: no more planned breaking changes
+- :heavy_check_mark: countdown to a stable release
+- :x: feature-complete
+- :x: tutorials
+
+Watch [the announcement](https://www.youtube.com/watch?v=A8jkJTWacow&t=29628s) and track [progress towards 1.0](https://github.com/sveltejs/kit/issues?q=is%3Aopen+is%3Aissue+milestone%3A1.0) for more details.
 
 ## Overview
 
diff --git a/documentation/docs/00-introduction.md b/documentation/docs/00-introduction.md
index afe9aec69..3278c0469 100644
--- a/documentation/docs/00-introduction.md
+++ b/documentation/docs/00-introduction.md
@@ -12,7 +12,7 @@ title: Introduction
 
 SvelteKit is a framework for building extremely high-performance web apps.
 
-Building an app with all the modern best practices is fiendishly complicated. Those practices include [build optimizations](https://vitejs.dev/guide/features.html#build-optimizations), so that you load only the minimal required code; [offline support](/docs/service-workers); [prefetching](/docs/link-options#data-sveltekit-prefetch) pages before the user initiates navigation; and [configurable rendering](/docs/page-options) that allows you to render your app [on the server](/docs/appendix#ssr) or [in the browser](/docs/appendix#csr) at runtime or [at build-time](/docs/page-options#prerender). SvelteKit does all the boring stuff for you so that you can get on with the creative part.
+Building an app with all the modern best practices is fiendishly complicated. Those practices include [build optimizations](https://vitejs.dev/guide/features.html#build-optimizations), so that you load only the minimal required code; [offline support](/docs/service-workers); [prefetching](/docs/link-options#data-sveltekit-prefetch) pages before the user initiates navigation; and [configurable rendering](/docs/page-options) that allows you to render your app [on the server](/docs/appendix#ssr) or [in the browser](/docs/appendix#csr-and-spa) at runtime or [at build-time](/docs/appendix#prerendering). SvelteKit does all the boring stuff for you so that you can get on with the creative part.
 
 It uses [Vite](https://vitejs.dev/) with a [Svelte plugin](https://github.com/sveltejs/vite-plugin-svelte) to provide a lightning-fast and feature-rich development experience with [Hot Module Replacement (HMR)](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#hot), where changes to your code are reflected in the browser instantly.
 
@@ -29,7 +29,7 @@ npm install
 npm run dev
 ```
 
-The first command will scaffold a new project in the `my-app` directory asking you if you'd like to set up some basic tooling such as TypeScript. See the FAQ for [pointers on setting up additional tooling](https://kit.svelte.dev/faq#integrations). The subsequent commands will then install its dependencies and start a server on [localhost:5173](http://localhost:5173).
+The first command will scaffold a new project in the `my-app` directory asking you if you'd like to set up some basic tooling such as TypeScript. See the FAQ for [pointers on setting up additional tooling](/faq#integrations). The subsequent commands will then install its dependencies and start a server on [localhost:5173](http://localhost:5173).
 
 There are two basic concepts:
 
diff --git a/documentation/docs/01-project-structure.md b/documentation/docs/01-project-structure.md
index f035f7368..b4d7c787b 100644
--- a/documentation/docs/01-project-structure.md
+++ b/documentation/docs/01-project-structure.md
@@ -8,6 +8,8 @@ A typical SvelteKit project looks like this:
 my-project/
 ├ src/
 │ ├ lib/
+│ │ ├ server/
+│ │ │ └ [your server-only lib files]
 │ │ └ [your lib files]
 │ ├ params/
 │ │ └ [your param matchers]
@@ -35,12 +37,13 @@ You'll also find common files like `.gitignore` and `.npmrc` (and `.prettierrc`
 The `src` directory contains the meat of your project.
 
 - `lib` contains your library code, which can be imported via the [`$lib`](/docs/modules#$lib) alias, or packaged up for distribution using [`svelte-package`](/docs/packaging)
+  - `server` contains your server-only library code. It can be imported by using the [`$lib/server`](/docs/server-only-modules) alias. SvelteKit will prevent you from importing these in client code.
 - `params` contains any [param matchers](/docs/advanced-routing#matching) your app needs
 - `routes` contains the [routes](/docs/routing) of your application
 - `app.html` is your page template — an HTML document containing the following placeholders:
   - `%sveltekit.head%` — `<link>` and `<script>` elements needed by the app, plus any `<svelte:head>` content
   - `%sveltekit.body%` — the markup for a rendered page. Typically this lives inside a `<div>` or other element, rather than directly inside `<body>`, to prevent bugs caused by browser extensions injecting elements that are then destroyed by the hydration process
-  - `%sveltekit.assets%` — either [`paths.assets`](/docs/configuration#paths), if specified, or a relative path to [`paths.base`](/docs/configuration#base)
+  - `%sveltekit.assets%` — either [`paths.assets`](/docs/configuration#paths), if specified, or a relative path to [`paths.base`](/docs/configuration#paths)
   - `%sveltekit.nonce%` — a [CSP](/docs/configuration#csp) nonce for manually included links and scripts, if used
 - `error.html` (optional) is the page that is rendered when everything else fails. It can contain the following placeholders:
   - `%sveltekit.status%` — the HTTP status
@@ -78,10 +81,6 @@ A SvelteKit project is really just a [Vite](https://vitejs.dev) project that use
 
 ### Other files
 
-#### test
-
-If you choose to add tests during `npm create svelte@latest`, they will go in a `test` directory.
-
 #### .svelte-kit
 
 As you develop and build your project, SvelteKit will generate files in a `.svelte-kit` directory (configurable as [`outDir`](/docs/configuration#outdir)). You can ignore its contents, and delete them at any time (they will be regenerated when you next `dev` or `build`).
diff --git a/documentation/docs/02-web-standards.md b/documentation/docs/02-web-standards.md
index 8c5c09a18..1d68b36fc 100644
--- a/documentation/docs/02-web-standards.md
+++ b/documentation/docs/02-web-standards.md
@@ -51,7 +51,7 @@ Most of the time, your endpoints will return complete data, as in the `userAgent
 
 ### URL APIs
 
-URLs are represented by the [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) interface, which includes useful properties like `origin` and `pathname` (and, in the browser, `hash`). This interface shows up in various places — `event.url` in [hooks](/docs/hooks) and [server routes](/docs/routing#server), [`$page.url`](/docs/modules#$app-stores) in [pages](/docs/routing#pages), `from` and `to` in [`beforeNavigate` and `afterNavigate`](/docs/modules#$app-navigation) and so on.
+URLs are represented by the [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) interface, which includes useful properties like `origin` and `pathname` (and, in the browser, `hash`). This interface shows up in various places — `event.url` in [hooks](/docs/hooks) and [server routes](/docs/routing#server), [`$page.url`](/docs/modules#$app-stores) in [pages](/docs/routing#page), `from` and `to` in [`beforeNavigate` and `afterNavigate`](/docs/modules#$app-navigation) and so on.
 
 #### URLSearchParams
 
diff --git a/documentation/docs/03-routing.md b/documentation/docs/03-routing.md
index 7c17d59a0..ac5598dd7 100644
--- a/documentation/docs/03-routing.md
+++ b/documentation/docs/03-routing.md
@@ -127,7 +127,7 @@ If an error occurs during `load`, SvelteKit will render a default error page. Yo
 <h1>{$page.status}: {$page.error.message}</h1>
 ```
 
-SvelteKit will 'walk up the tree' looking for the closest error boundary — if the file above didn't exist it would try `src/routes/blog/+error.svelte` and `src/routes/+error.svelte` before rendering the default error page. If _that_ fails, SvelteKit will bail out and render a static fallback error page, which you can customise by creating a `src/error.html` file.
+SvelteKit will 'walk up the tree' looking for the closest error boundary — if the file above didn't exist it would try `src/routes/blog/+error.svelte` and `src/routes/+error.svelte` before rendering the default error page. If _that_ fails (or if the error was thrown from the `load` function of the root `+layout`, which sits 'above' the root `+error`), SvelteKit will bail out and render a static fallback error page, which you can customise by creating a `src/error.html` file.
 
 ### +layout
 
diff --git a/documentation/docs/04-advanced-routing.md b/documentation/docs/04-advanced-routing.md
index fc665576d..05c1c813f 100644
--- a/documentation/docs/04-advanced-routing.md
+++ b/documentation/docs/04-advanced-routing.md
@@ -47,7 +47,7 @@ src/routes/
 └ +error.svelte
 ```
 
-> `src/routes/a/[...rest]/z/+page.svelte` will match `/a/z` (i.e. there's no parameter at all) as well as `/a/b/z` and `/a/b/c/z` and so on. Make sure you check that the value of the rest parameter is valid, for example using a [matcher](#advanced-routing-matching).
+> `src/routes/a/[...rest]/z/+page.svelte` will match `/a/z` (i.e. there's no parameter at all) as well as `/a/b/z` and `/a/b/c/z` and so on. Make sure you check that the value of the rest parameter is valid, for example using a [matcher](#matching).
 
 ### Matching
 
@@ -88,7 +88,7 @@ SvelteKit needs to know which route is being requested. To do so, it sorts them
 
 - More specific routes are higher priority (e.g. a route with no parameters is more specific than a route with one dynamic parameter, and so on)
 - `+server` files have higher priority than `+page` files
-- Parameters with [matchers](#advanced-routing-matching) (`[name=type]`) are higher priority than those without (`[name]`)
+- Parameters with [matchers](#matching) (`[name=type]`) are higher priority than those without (`[name]`)
 - Rest parameters have lowest priority
 - Ties are resolved alphabetically
 
diff --git a/documentation/docs/05-load.md b/documentation/docs/05-load.md
index 7bcf13c70..d048a2f7a 100644
--- a/documentation/docs/05-load.md
+++ b/documentation/docs/05-load.md
@@ -143,7 +143,7 @@ export async function load({ depends }) {
 - it can be used to make credentialed requests on the server, as it inherits the `cookie` and `authorization` headers for the page request
 - it can make relative requests on the server (ordinarily, `fetch` requires a URL with an origin when used in a server context)
 - internal requests (e.g. for `+server.js` routes) go direct to the handler function when running on the server, without the overhead of an HTTP call
-- during server-side rendering, the response will be captured and inlined into the rendered HTML. Note that headers will _not_ be serialized, unless explicitly included via [`filterSerializedResponseHeaders`](/docs/hooks#hooks-server-js-handle)
+- during server-side rendering, the response will be captured and inlined into the rendered HTML. Note that headers will _not_ be serialized, unless explicitly included via [`filterSerializedResponseHeaders`](/docs/hooks#server-hooks-handle)
 - during hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request
 
 > Cookies will only be passed through if the target host is the same as the SvelteKit application or a more specific subdomain of it.
@@ -308,7 +308,7 @@ export function load({ locals }) {
 }
 ```
 
-If an _unexpected_ error is thrown, SvelteKit will invoke [`handleError`](/docs/hooks#hooks-server-js-handleerror) and treat it as a 500 Internal Error.
+If an _unexpected_ error is thrown, SvelteKit will invoke [`handleError`](/docs/hooks#shared-hooks-handleerror) and treat it as a 500 Internal Error.
 
 > In development, stack traces for unexpected errors are visible as `$page.error.stack`. In production, stack traces are hidden.
 
@@ -343,7 +343,7 @@ A `load` function will re-run in the following situations:
 - It references a property of `params` whose value has changed
 - It references a property of `url` (such as `url.pathname` or `url.search`) whose value has changed
 - It calls `await parent()` and a parent `load` function re-ran
-- It declared a dependency on a specific URL via [`fetch`](#fetch) or [`depends`](#depends), and that URL was marked invalid with [`invalidate(url)`](/docs/modules#$app-navigation-invalidate)
+- It declared a dependency on a specific URL via [`fetch`](#input-methods-fetch) or [`depends`](#input-methods-depends), and that URL was marked invalid with [`invalidate(url)`](/docs/modules#$app-navigation-invalidate)
 - All active `load` functions were forcibly re-run with [`invalidate()`](/docs/modules#$app-navigation-invalidate)
 
 If a `load` function is triggered to re-run, the page will not remount — instead, it will update with the new `data`. This means that components' internal state is preserved. If this isn't want you want, you can reset whatever you need to reset inside an [`afterNavigate`](/docs/modules#$app-navigation-afternavigate) callback, and/or wrap your component in a [`{#key ...}`](https://svelte.dev/docs#template-syntax-key) block.
diff --git a/documentation/docs/07-hooks.md b/documentation/docs/07-hooks.md
index 827d9e7a7..810ac0ebe 100644
--- a/documentation/docs/07-hooks.md
+++ b/documentation/docs/07-hooks.md
@@ -9,6 +9,8 @@ There are two hooks files, both optional:
 - `src/hooks.server.js` — your app's server hooks
 - `src/hooks.client.js` — your app's client hooks
 
+Code in these modules will run when the application starts up, making them useful for initializing database clients and so on.
+
 > You can configure the location of these files with [`config.kit.files.hooks`](/docs/configuration#files).
 
 ### Server hooks
@@ -90,7 +92,7 @@ export async function handle({ event, resolve }) {
 
 #### handleFetch
 
-This function allows you to modify (or replace) a `fetch` request for an external resource that happens inside a `load` function that runs on the server (or during pre-rendering).
+This function allows you to modify (or replace) a `fetch` request that happens inside a `load` function that runs on the server (or during pre-rendering).
 
 Or your `load` function might make a request to a public URL like `https://api.yourapp.com` when the user performs a client-side navigation to the respective page, but during SSR it might make sense to hit the API directly (bypassing whatever proxies and load balancers sit between it and the public internet).
 
@@ -140,6 +142,18 @@ If an unexpected error is thrown during loading or rendering, this function will
 - you can log the error
 - you can generate a custom representation of the error that is safe to show to users, omitting sensitive details like messages and stack traces. The returned value, which defaults to `{ message: 'Internal Error' }`, becomes the value of `$page.error`. To make this type-safe, you can customize the expected shape by declaring an `App.PageError` interface (which must include `message: string`, to guarantee sensible fallback behavior).
 
+The following code shows an example of typing the error shape as `{ message: string; code: string }` and returning it accordingly from the `handleError` functions:
+
+```ts
+/// file: src/app.d.ts
+declare namespace App {
+	interface PageError {
+		message: string;
+		code: string;
+	}
+}
+```
+
 ```js
 /// file: src/hooks.server.js
 // @errors: 2322 2571
@@ -160,8 +174,28 @@ export function handleError({ error, event }) {
 }
 ```
 
+```js
+/// file: src/hooks.client.js
+// @errors: 2322 2571
+// @filename: ambient.d.ts
+const Sentry: any;
+
+// @filename: index.js
+// ---cut---
+/** @type {import('@sveltejs/kit').HandleClientError} */
+export function handleError({ error, event }) {
+	// example integration with https://sentry.io/
+	Sentry.captureException(error, { event });
+
+	return {
+		message: 'Whoops!',
+		code: error.code ?? 'UNKNOWN'
+	};
+}
+```
+
 > In `src/hooks.client.js`, the type of `handleError` is `HandleClientError` instead of `HandleServerError`, and `event` is a `NavigationEvent` rather than a `RequestEvent`.
 
 This function is not called for _expected_ errors (those thrown with the [`error`](/docs/modules#sveltejs-kit-error) function imported from `@sveltejs/kit`).
 
-During development, if an error occurs because of a syntax error in your Svelte code, a `frame` property will be appended highlighting the location of the error.
\ No newline at end of file
+During development, if an error occurs because of a syntax error in your Svelte code, the passed in error has a `frame` property appended highlighting the location of the error.
diff --git a/documentation/docs/08-server-only-modules.md b/documentation/docs/08-server-only-modules.md
new file mode 100644
index 000000000..3017186a7
--- /dev/null
+++ b/documentation/docs/08-server-only-modules.md
@@ -0,0 +1,54 @@
+---
+title: Server-only modules
+---
+
+Like a good friend, SvelteKit keeps your secrets. When writing your backend and frontend in the same repository, it can be easy to accidentally import sensitive data into your front-end code (environment variables containing API keys, for example). SvelteKit provides a way to prevent this entirely: server-only modules.
+
+### Private environment variables
+
+The `$env/static/private` and `$env/dynamic/private` modules, which are covered in the [modules](/docs/modules) section, can only be imported into modules that only run on the server, such as [`hooks.server.js`](/docs/hooks#server-hooks) or [`+page.server.js`](/docs/routing#page-page-server-js).
+
+### Your modules
+
+You can make your own modules server-only in two ways:
+
+- adding `.server` to the filename, e.g. `secrets.server.js`
+- placing them in `$lib/server`, e.g. `$lib/server/secrets.js`
+
+### How it works
+
+Any time you have public-facing code that imports server-only code (whether directly or indirectly)...
+
+```js
+// @errors: 7005
+/// file: $lib/server/secrets.js
+export const atlantisCoordinates = [/* redacted */];
+```
+
+```js
+// @errors: 2307 7006
+/// file: src/routes/utils.js
+export { atlantisCoordinates } from '$lib/server/secrets.js';
+
+export const add = (a, b) => a + b;
+```
+
+```html
+/// file: src/routes/+page.svelte
+<script>
+	import { add } from './utils.js';
+</script>
+```
+
+...SvelteKit will error:
+
+```
+Cannot import $lib/server/secrets.js into public-facing code:
+- src/routes/+page.svelte
+	- src/routes/utils.js
+		- $lib/server/secrets.js
+```
+
+Even though the public-facing code — `src/routes/+page.svelte` — only uses the `add` export and not the secret `atlantisCoordinates` export, the secret code could end up in JavaScript that the browser downloads, and so the import chain is considered unsafe.
+
+This feature also works with dynamic imports, even interpolated ones like ``await import(`./${foo}.js`)``, with one small caveat: during development, if there are two or more dynamic imports between the public-facing code and the server-only module, the illegal import will not be detected the first time the code is loaded.
\ No newline at end of file
diff --git a/documentation/docs/08-modules.md b/documentation/docs/09-modules.md
similarity index 100%
rename from documentation/docs/08-modules.md
rename to documentation/docs/09-modules.md
diff --git a/documentation/docs/09-service-workers.md b/documentation/docs/10-service-workers.md
similarity index 100%
rename from documentation/docs/09-service-workers.md
rename to documentation/docs/10-service-workers.md
diff --git a/documentation/docs/10-link-options.md b/documentation/docs/11-link-options.md
similarity index 100%
rename from documentation/docs/10-link-options.md
rename to documentation/docs/11-link-options.md
diff --git a/documentation/docs/11-adapters.md b/documentation/docs/12-adapters.md
similarity index 95%
rename from documentation/docs/11-adapters.md
rename to documentation/docs/12-adapters.md
index 2195b3354..80bee1dd6 100644
--- a/documentation/docs/11-adapters.md
+++ b/documentation/docs/12-adapters.md
@@ -54,7 +54,7 @@ Most adapters will generate static HTML for any [prerenderable](/docs/page-optio
 
 You can also use `adapter-static` to generate single-page apps (SPAs) by specifying a [fallback page](https://github.com/sveltejs/kit/tree/master/packages/adapter-static#spa-mode).
 
-> You must ensure [`trailingSlash`](configuration#trailingslash) is set appropriately for your environment. If your host does not render `/a.html` upon receiving a request for `/a` then you will need to set `trailingSlash: 'always'` to create `/a/index.html` instead.
+> You must ensure [`trailingSlash`](/docs/configuration#trailingslash) is set appropriately for your environment. If your host does not render `/a.html` upon receiving a request for `/a` then you will need to set `trailingSlash: 'always'` to create `/a/index.html` instead.
 
 #### Platform-specific context
 
diff --git a/documentation/docs/12-page-options.md b/documentation/docs/13-page-options.md
similarity index 95%
rename from documentation/docs/12-page-options.md
rename to documentation/docs/13-page-options.md
index 9569e1671..9303fc078 100644
--- a/documentation/docs/12-page-options.md
+++ b/documentation/docs/13-page-options.md
@@ -2,7 +2,7 @@
 title: Page options
 ---
 
-By default, SvelteKit will render (or [prerender](/docs/appendix#prerendering)) any component first on the server and send it to the client as HTML. It will then render the component again in the browser to make it interactive in a process called **hydration**. For this reason, you need to ensure that components can run in both places. SvelteKit will then initialise a [**router**](/docs/routing) that takes over subsequent navigations.
+By default, SvelteKit will render (or [prerender](/docs/appendix#prerendering)) any component first on the server and send it to the client as HTML. It will then render the component again in the browser to make it interactive in a process called **hydration**. For this reason, you need to ensure that components can run in both places. SvelteKit will then initialize a [**router**](/docs/routing) that takes over subsequent navigations.
 
 You can control each of these on a page-by-page basis by exporting options from [`+page.js`](/docs/routing#page-page-js) or [`+page.server.js`](/docs/routing#page-page-server-js), or for groups of pages using a shared [`+layout.js`](/docs/routing#layout-layout-js) or [`+layout.server.js`](/docs/routing#layout-layout-server-js). To define an option for the whole app, export it from the root layout. Child layouts and pages override values set in parent layouts, so — for example — you can enable prerendering for your entire app then disable it for pages that need to be dynamically rendered.
 
@@ -58,7 +58,7 @@ The basic rule is this: for a page to be prerenderable, any two users hitting it
 
 Note that you can still prerender pages that load data based on the page's parameters, such as a `src/routes/blog/[slug]/+page.svelte` route.
 
-Accessing [`url.searchParams`](/docs/load#input-url) during prerendering is forbidden. If you need to use it, ensure you are only doing so in the browser (for example in `onMount`).
+Accessing [`url.searchParams`](/docs/load#input-properties-url) during prerendering is forbidden. If you need to use it, ensure you are only doing so in the browser (for example in `onMount`).
 
 #### Route conflicts
 
diff --git a/documentation/docs/13-packaging.md b/documentation/docs/14-packaging.md
similarity index 100%
rename from documentation/docs/13-packaging.md
rename to documentation/docs/14-packaging.md
diff --git a/documentation/docs/14-cli.md b/documentation/docs/15-cli.md
similarity index 100%
rename from documentation/docs/14-cli.md
rename to documentation/docs/15-cli.md
diff --git a/documentation/docs/15-configuration.md b/documentation/docs/16-configuration.md
similarity index 99%
rename from documentation/docs/15-configuration.md
rename to documentation/docs/16-configuration.md
index ad5dd2bd6..eb62e504b 100644
--- a/documentation/docs/15-configuration.md
+++ b/documentation/docs/16-configuration.md
@@ -184,7 +184,7 @@ An object containing zero or more of the following `string` values:
 - `assets` — a place to put static files that should have stable URLs and undergo no processing, such as `favicon.ico` or `manifest.json`
 - `hooks` — the location of your client and server hooks (see [Hooks](/docs/hooks))
 - `lib` — your app's internal library, accessible throughout the codebase as `$lib`
-- `params` — a directory containing [parameter matchers](/docs/routing#advanced-routing-matching)
+- `params` — a directory containing [parameter matchers](/docs/advanced-routing#matching)
 - `routes` — the files that define the structure of your app (see [Routing](/docs/routing))
 - `serviceWorker` — the location of your service worker's entry point (see [Service workers](/docs/service-workers))
 - `template` — the location of the template for HTML responses
@@ -299,7 +299,7 @@ Whether to remove, append, or ignore trailing slashes when resolving URLs (note
 
 This option also affects [prerendering](/docs/page-options#prerender). If `trailingSlash` is `always`, a route like `/about` will result in an `about/index.html` file, otherwise it will create `about.html`, mirroring static webserver conventions.
 
-> Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO. If you use this option, ensure that you implement logic for conditionally adding or removing trailing slashes from `request.path` inside your [`handle`](/docs/hooks#hooks-server-js-handle) function.
+> Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO. If you use this option, ensure that you implement logic for conditionally adding or removing trailing slashes from `request.path` inside your [`handle`](/docs/hooks#server-hooks-handle) function.
 
 ### version
 
diff --git a/documentation/docs/16-types.md b/documentation/docs/17-types.md
similarity index 100%
rename from documentation/docs/16-types.md
rename to documentation/docs/17-types.md
diff --git a/documentation/docs/17-seo.md b/documentation/docs/18-seo.md
similarity index 95%
rename from documentation/docs/17-seo.md
rename to documentation/docs/18-seo.md
index 0a6bb5cf1..5ae9b4333 100644
--- a/documentation/docs/17-seo.md
+++ b/documentation/docs/18-seo.md
@@ -8,7 +8,7 @@ The most important aspect of SEO is to create high-quality content that is widel
 
 #### SSR
 
-While search engines have got better in recent years at indexing content that was rendered with client-side JavaScript, server-side rendered content is indexed more frequently and reliably. SvelteKit employs SSR by default, and while you can disable it in [`handle`](/docs/hooks#hooks-server-js-handle), you should leave it on unless you have a good reason not to.
+While search engines have got better in recent years at indexing content that was rendered with client-side JavaScript, server-side rendered content is indexed more frequently and reliably. SvelteKit employs SSR by default, and while you can disable it in [`handle`](/docs/hooks#server-hooks-handle), you should leave it on unless you have a good reason not to.
 
 > SvelteKit's rendering is highly configurable and you can implement [dynamic rendering](https://developers.google.com/search/docs/advanced/javascript/dynamic-rendering) if necessary. It's not generally recommended, since SSR has other benefits beyond SEO.
 
@@ -18,7 +18,7 @@ Signals such as [Core Web Vitals](https://web.dev/vitals/#core-web-vitals) impac
 
 #### Normalized URLs
 
-SvelteKit redirects pathnames with trailing slashes to ones without (or vice versa depending on your [configuration](configuration#trailingslash)), as duplicate URLs are bad for SEO.
+SvelteKit redirects pathnames with trailing slashes to ones without (or vice versa depending on your [configuration](/docs/configuration#trailingslash)), as duplicate URLs are bad for SEO.
 
 ### Manual setup
 
diff --git a/documentation/docs/18-assets.md b/documentation/docs/19-assets.md
similarity index 100%
rename from documentation/docs/18-assets.md
rename to documentation/docs/19-assets.md
diff --git a/documentation/docs/19-accessibility.md b/documentation/docs/20-accessibility.md
similarity index 97%
rename from documentation/docs/19-accessibility.md
rename to documentation/docs/20-accessibility.md
index b015eb7e5..b725f320f 100644
--- a/documentation/docs/19-accessibility.md
+++ b/documentation/docs/20-accessibility.md
@@ -54,7 +54,7 @@ By default, SvelteKit's page template sets the default language of the document
 <html lang="de">
 ```
 
-If your content is available in multiple languages, you should set the `lang` attribute based on the language of the current page. You can do this with SvelteKit's [handle hook](/docs/hooks#hooks-server-js-handle):
+If your content is available in multiple languages, you should set the `lang` attribute based on the language of the current page. You can do this with SvelteKit's [handle hook](/docs/hooks#server-hooks-handle):
 
 ```html
 /// file: src/app.html
@@ -62,7 +62,7 @@ If your content is available in multiple languages, you should set the `lang` at
 ```
 
 ```js
-/// file: src/hooks.js
+/// file: src/hooks.server.js
 /**
  * @param {import('@sveltejs/kit').RequestEvent} event
  * @returns {string}
diff --git a/documentation/docs/80-migrating.md b/documentation/docs/80-migrating.md
index f3b17d1e3..7d8750494 100644
--- a/documentation/docs/80-migrating.md
+++ b/documentation/docs/80-migrating.md
@@ -5,7 +5,7 @@ rank: 1
 
 SvelteKit is the successor to Sapper and shares many elements of its design.
 
-If you have an existing Sapper app that you plan to migrate to SvelteKit, there are a number of changes you will need to make. You may find it helpful to view [some examples](https://kit.svelte.dev/docs/additional-resources#examples) while migrating.
+If you have an existing Sapper app that you plan to migrate to SvelteKit, there are a number of changes you will need to make. You may find it helpful to view [some examples](/docs/additional-resources#examples) while migrating.
 
 ### package.json
 
@@ -41,7 +41,7 @@ Your `webpack.config.js` or `rollup.config.js` should be replaced with a `svelte
 
 You will need to add an [adapter](/docs/adapters). `sapper build` is roughly equivalent to [adapter-node](https://github.com/sveltejs/kit/tree/master/packages/adapter-node) while `sapper export` is roughly equivalent to [adapter-static](https://github.com/sveltejs/kit/tree/master/packages/adapter-static), though you might prefer to use an adapter designed for the platform you're deploying to.
 
-If you were using plugins for filetypes that are not automatically handled by [Vite](https://vitejs.dev), you will need to find Vite equivalents and add them to the [Vite config](/docs/configuration#vite).
+If you were using plugins for filetypes that are not automatically handled by [Vite](https://vitejs.dev), you will need to find Vite equivalents and add them to the [Vite config](/docs/project-structure#project-files-vite-config-js).
 
 #### src/client.js
 
@@ -81,7 +81,7 @@ Routes now are made up of the folder name exclusively to remove ambiguity, the f
 | routes/about/index.svelte | routes/about/+page.svelte |
 | routes/about.svelte       | routes/about/+page.svelte |
 
-Your custom error page component should be renamed from `_error.svelte` to `+error.svelte`. Any `_layout.svelte` files should likewise be renamed `+layout.svelte`. [Any other files are ignored](https://kit.svelte.dev/docs/routing#other-files).
+Your custom error page component should be renamed from `_error.svelte` to `+error.svelte`. Any `_layout.svelte` files should likewise be renamed `+layout.svelte`. [Any other files are ignored](/docs/routing#other-files).
 
 #### Imports
 
@@ -97,7 +97,7 @@ As before, pages and layouts can export a function that allows data to be loaded
 
 This function has been renamed from `preload` to [`load`](/docs/load), it now lives in a `+page.js` (or `+layout.js`) next to its `+page.svelte` (or `+layout.svelte`), and its API has changed. Instead of two arguments — `page` and `session` — there is a single `event` argument.
 
-There is no more `this` object, and consequently no `this.fetch`, `this.error` or `this.redirect`. Instead, you can get [`fetch`](https://kit.svelte.dev/docs/load#input-methods-fetch) from the input methods, and both [`error`](https://kit.svelte.dev/docs/load#errors) and [`redirect`](https://kit.svelte.dev/docs/load#redirects) are now thrown.
+There is no more `this` object, and consequently no `this.fetch`, `this.error` or `this.redirect`. Instead, you can get [`fetch`](/docs/load#input-methods-fetch) from the input methods, and both [`error`](/docs/load#errors) and [`redirect`](/docs/load#redirects) are now thrown.
 
 #### Stores
 
@@ -119,7 +119,7 @@ You access them differently in SvelteKit. `stores` is now `getStores`, but in mo
 
 #### Routing
 
-Regex routes are no longer supported. Instead, use [advanced route matching](/docs/routing#advanced-routing-matching).
+Regex routes are no longer supported. Instead, use [advanced route matching](/docs/advanced-routing#matching).
 
 #### Segments
 
@@ -150,7 +150,7 @@ See [the FAQ](/faq#integrations) for detailed information about integrations.
 
 #### HTML minifier
 
-Sapper includes `html-minifier` by default. SvelteKit does not include this, but it can be added as a [hook](/docs/hooks#hooks-server-js-handle):
+Sapper includes `html-minifier` by default. SvelteKit does not include this, but it can be added as a [hook](/docs/hooks#server-hooks-handle):
 
 ```js
 // @filename: ambient.d.ts
diff --git a/documentation/faq/10-version-numbers.md b/documentation/faq/10-version-numbers.md
index 0d01947d6..143956b57 100644
--- a/documentation/faq/10-version-numbers.md
+++ b/documentation/faq/10-version-numbers.md
@@ -1,5 +1,5 @@
 ---
-title: SvelteKit is not 1.0 yet. Should I use it? What about Sapper?
+title: SvelteKit is not 1.0 yet. Should I use it?
 ---
 
-SvelteKit is currently in beta while we finalize the APIs. No new features will be added to Sapper and all development will be focused on SvelteKit.
+SvelteKit is currently in release candidate phase. We expect minimal to no breaking changes and for development to be focused on bug fixes.
diff --git a/package.json b/package.json
index b71961a9e..f3a6deab9 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
 		"turbo": "^1.3.4",
 		"typescript": "^4.8.2"
 	},
-	"packageManager": "pnpm@7.9.5",
+	"packageManager": "pnpm@7.11.0",
 	"engines": {
 		"pnpm": "^7.0.0"
 	},
diff --git a/packages/adapter-auto/CHANGELOG.md b/packages/adapter-auto/CHANGELOG.md
index 3f061b1de..d74f6736a 100644
--- a/packages/adapter-auto/CHANGELOG.md
+++ b/packages/adapter-auto/CHANGELOG.md
@@ -1,5 +1,22 @@
 # @sveltejs/adapter-auto
 
+## 1.0.0-next.73
+
+### Patch Changes
+
+- Updated dependencies [[`b32b3ac9`](https://github.com/sveltejs/kit/commit/b32b3ac9c05efebfab5651de9bf5cdd0d4d54c12), [`ee5777e3`](https://github.com/sveltejs/kit/commit/ee5777e33695d2ddf1742a5e1d0144674d48d2d3)]:
+  - @sveltejs/adapter-cloudflare@1.0.0-next.33
+  - @sveltejs/adapter-netlify@1.0.0-next.77
+  - @sveltejs/adapter-vercel@1.0.0-next.74
+
+## 1.0.0-next.72
+
+### Patch Changes
+
+- Updated dependencies [[`f1e762bf`](https://github.com/sveltejs/kit/commit/f1e762bff16acc56b0d1e10c9cadeada0d25017d)]:
+  - @sveltejs/adapter-vercel@1.0.0-next.73
+  - @sveltejs/adapter-netlify@1.0.0-next.76
+
 ## 1.0.0-next.71
 
 ### Patch Changes
diff --git a/packages/adapter-auto/README.md b/packages/adapter-auto/README.md
index b79d49a13..6bc8e45dc 100644
--- a/packages/adapter-auto/README.md
+++ b/packages/adapter-auto/README.md
@@ -9,6 +9,7 @@ The following environments are supported out-of-the-box, meaning a newly created
 - [Cloudflare Pages](https://developers.cloudflare.com/pages/) via [adapter-cloudflare](../adapter-cloudflare)
 - [Netlify](https://netlify.com/) via [adapter-netlify](../adapter-netlify)
 - [Vercel](https://vercel.com/) via [adapter-vercel](../adapter-vercel)
+- [Azure Static Web Apps](https://docs.microsoft.com/en-us/azure/static-web-apps/) via [svelte-adapter-azure-swa](https://github.com/geoffrich/svelte-adapter-azure-swa)
 
 ## Community adapters
 
diff --git a/packages/adapter-auto/adapters.js b/packages/adapter-auto/adapters.js
index e018e7462..12b77910e 100644
--- a/packages/adapter-auto/adapters.js
+++ b/packages/adapter-auto/adapters.js
@@ -13,5 +13,10 @@ export const adapters = [
 		name: 'Netlify',
 		test: () => !!process.env.NETLIFY,
 		module: '@sveltejs/adapter-netlify'
+	},
+	{
+		name: 'Azure Static Web Apps',
+		test: () => process.env.GITHUB_ACTION_REPOSITORY === 'Azure/static-web-apps-deploy',
+		module: 'svelte-adapter-azure-swa'
 	}
 ];
diff --git a/packages/adapter-auto/package.json b/packages/adapter-auto/package.json
index 69258ebbf..d200d512c 100644
--- a/packages/adapter-auto/package.json
+++ b/packages/adapter-auto/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-auto",
-	"version": "1.0.0-next.71",
+	"version": "1.0.0-next.73",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-cloudflare-workers/CHANGELOG.md b/packages/adapter-cloudflare-workers/CHANGELOG.md
index 334709210..2a0d9c898 100644
--- a/packages/adapter-cloudflare-workers/CHANGELOG.md
+++ b/packages/adapter-cloudflare-workers/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-cloudflare-workers
 
+## 1.0.0-next.55
+
+### Patch Changes
+
+- Update to esbuild 0.15 ([#6740](https://github.com/sveltejs/kit/pull/6740))
+
 ## 1.0.0-next.54
 
 ### Patch Changes
diff --git a/packages/adapter-cloudflare-workers/package.json b/packages/adapter-cloudflare-workers/package.json
index 47bb3a070..769ed805c 100644
--- a/packages/adapter-cloudflare-workers/package.json
+++ b/packages/adapter-cloudflare-workers/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-cloudflare-workers",
-	"version": "1.0.0-next.54",
+	"version": "1.0.0-next.55",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -30,7 +30,7 @@
 	"dependencies": {
 		"@cloudflare/workers-types": "^3.14.0",
 		"@iarna/toml": "^2.2.5",
-		"esbuild": "^0.14.48"
+		"esbuild": "^0.15.6"
 	},
 	"devDependencies": {
 		"@cloudflare/kv-asset-handler": "^0.2.0",
diff --git a/packages/adapter-cloudflare/CHANGELOG.md b/packages/adapter-cloudflare/CHANGELOG.md
index ad4e408b8..b3691a46d 100644
--- a/packages/adapter-cloudflare/CHANGELOG.md
+++ b/packages/adapter-cloudflare/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-cloudflare
 
+## 1.0.0-next.33
+
+### Patch Changes
+
+- Update to esbuild 0.15 ([#6740](https://github.com/sveltejs/kit/pull/6740))
+
 ## 1.0.0-next.32
 
 ### Patch Changes
diff --git a/packages/adapter-cloudflare/README.md b/packages/adapter-cloudflare/README.md
index ac335d69b..d1ed0857a 100644
--- a/packages/adapter-cloudflare/README.md
+++ b/packages/adapter-cloudflare/README.md
@@ -81,11 +81,11 @@ declare namespace App {
 
 ## Notes
 
-Functions contained in the `/functions` directory at the project's root will _not_ be included in the deployment, which is compiled to a [single `_worker.js` file](https://developers.cloudflare.com/pages/platform/functions/#advanced-mode). Functions should be implemented as [endpoints](https://kit.svelte.dev/docs/routing#endpoints) in your SvelteKit app.
+Functions contained in the `/functions` directory at the project's root will _not_ be included in the deployment, which is compiled to a [single `_worker.js` file](https://developers.cloudflare.com/pages/platform/functions/#advanced-mode). Functions should be implemented as [server endpoints](https://kit.svelte.dev/docs/routing#server) in your SvelteKit app.
 
 The [`_headers` and `_redirects`](config files) files specific to Cloudflare Pages can be used for static asset responses (like images) by putting them into the `/static` folder.
 
-However, they will have no effect on responses dynamically rendered by SvelteKit, which should return custom headers or redirect responses from [endpoints](https://kit.svelte.dev/docs/routing#endpoints) or with the [`handle`](https://kit.svelte.dev/docs/hooks#hooks-server-js-handle) hook.
+However, they will have no effect on responses dynamically rendered by SvelteKit, which should return custom headers or redirect responses from [server endpoints](https://kit.svelte.dev/docs/routing#server) or with the [`handle`](https://kit.svelte.dev/docs/hooks#server-hooks-handle) hook.
 
 ## Changelog
 
diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json
index 5054a4f17..89b35dd58 100644
--- a/packages/adapter-cloudflare/package.json
+++ b/packages/adapter-cloudflare/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-cloudflare",
-	"version": "1.0.0-next.32",
+	"version": "1.0.0-next.33",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -32,7 +32,7 @@
 	},
 	"dependencies": {
 		"@cloudflare/workers-types": "^3.14.0",
-		"esbuild": "^0.14.48",
+		"esbuild": "^0.15.6",
 		"worktop": "0.8.0-next.14"
 	},
 	"devDependencies": {
diff --git a/packages/adapter-netlify/CHANGELOG.md b/packages/adapter-netlify/CHANGELOG.md
index 52eadbe17..09d6eb432 100644
--- a/packages/adapter-netlify/CHANGELOG.md
+++ b/packages/adapter-netlify/CHANGELOG.md
@@ -1,5 +1,13 @@
 # @sveltejs/adapter-netlify
 
+## 1.0.0-next.77
+
+### Patch Changes
+
+- Update to esbuild 0.15 ([#6740](https://github.com/sveltejs/kit/pull/6740))
+
+* [feat] only generate ESM functions ([#6760](https://github.com/sveltejs/kit/pull/6760))
+
 ## 1.0.0-next.76
 
 ### Patch Changes
diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js
index cb99dfab4..6d77b17a7 100644
--- a/packages/adapter-netlify/index.js
+++ b/packages/adapter-netlify/index.js
@@ -1,7 +1,6 @@
 import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'fs';
 import { dirname, join, resolve, posix } from 'path';
 import { fileURLToPath } from 'url';
-import glob from 'tiny-glob/sync.js';
 import esbuild from 'esbuild';
 import toml from '@iarna/toml';
 
@@ -66,10 +65,6 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) {
 				`\n\n/${builder.config.kit.appDir}/immutable/*\n  cache-control: public\n  cache-control: immutable\n  cache-control: max-age=31536000\n`
 			);
 
-			// for esbuild, use ESM
-			// for zip-it-and-ship-it, use CJS until https://github.com/netlify/zip-it-and-ship-it/issues/750
-			const esm = netlify_config?.functions?.node_bundler === 'esbuild';
-
 			if (edge) {
 				if (split) {
 					throw new Error('Cannot use `split: true` alongside `edge: true`');
@@ -77,7 +72,7 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) {
 
 				await generate_edge_functions({ builder });
 			} else {
-				await generate_lambda_functions({ builder, esm, split, publish });
+				await generate_lambda_functions({ builder, split, publish });
 			}
 		}
 	};
@@ -148,9 +143,8 @@ async function generate_edge_functions({ builder }) {
  * @param {import('@sveltejs/kit').Builder} params.builder
  * @param { string } params.publish
  * @param { boolean } params.split
- * @param { boolean } params.esm
  */
-async function generate_lambda_functions({ builder, publish, split, esm }) {
+async function generate_lambda_functions({ builder, publish, split }) {
 	builder.mkdirp('.netlify/functions-internal');
 
 	/** @type {string[]} */
@@ -160,19 +154,11 @@ async function generate_lambda_functions({ builder, publish, split, esm }) {
 	const replace = {
 		'0SERVER': './server/index.js' // digit prefix prevents CJS build from using this as a variable name, which would also get replaced
 	};
-	if (esm) {
-		builder.copy(`${files}/esm`, '.netlify', { replace });
-	} else {
-		glob('**/*.js', { cwd: '.netlify/server', filesOnly: true }).forEach((file) => {
-			const filepath = `.netlify/server/${file}`;
-			const input = readFileSync(filepath, 'utf8');
-			const output = esbuild.transformSync(input, { format: 'cjs', target: 'node12' }).code;
-			writeFileSync(filepath, output);
-		});
 
-		builder.copy(`${files}/cjs`, '.netlify', { replace });
-		writeFileSync(join('.netlify', 'package.json'), JSON.stringify({ type: 'commonjs' }));
-	}
+	builder.copy(`${files}/esm`, '.netlify', { replace });
+
+	// Configuring the function to use ESM as the output format.
+	const fn_config = JSON.stringify({ config: { nodeModuleFormat: 'esm' }, version: 1 });
 
 	if (split) {
 		builder.log.minor('Generating serverless functions...');
@@ -201,14 +187,13 @@ async function generate_lambda_functions({ builder, publish, split, esm }) {
 				complete: (entry) => {
 					const manifest = entry.generateManifest({
 						relativePath: '../server',
-						format: esm ? 'esm' : 'cjs'
+						format: 'esm'
 					});
 
-					const fn = esm
-						? `import { init } from '../serverless.js';\n\nexport const handler = init(${manifest});\n`
-						: `const { init } = require('../serverless.js');\n\nexports.handler = init(${manifest});\n`;
+					const fn = `import { init } from '../serverless.js';\n\nexport const handler = init(${manifest});\n`;
 
-					writeFileSync(`.netlify/functions-internal/${name}.js`, fn);
+					writeFileSync(`.netlify/functions-internal/${name}.mjs`, fn);
+					writeFileSync(`.netlify/functions-internal/${name}.json`, fn_config);
 
 					redirects.push(`${pattern} /.netlify/functions/${name} 200`);
 					redirects.push(`${pattern}/__data.js /.netlify/functions/${name} 200`);
@@ -220,14 +205,13 @@ async function generate_lambda_functions({ builder, publish, split, esm }) {
 
 		const manifest = builder.generateManifest({
 			relativePath: '../server',
-			format: esm ? 'esm' : 'cjs'
+			format: 'esm'
 		});
 
-		const fn = esm
-			? `import { init } from '../serverless.js';\n\nexport const handler = init(${manifest});\n`
-			: `const { init } = require('../serverless.js');\n\nexports.handler = init(${manifest});\n`;
+		const fn = `import { init } from '../serverless.js';\n\nexport const handler = init(${manifest});\n`;
 
-		writeFileSync('.netlify/functions-internal/render.js', fn);
+		writeFileSync(`.netlify/functions-internal/render.json`, fn_config);
+		writeFileSync('.netlify/functions-internal/render.mjs', fn);
 		redirects.push('* /.netlify/functions/render 200');
 	}
 
diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json
index b459484f6..8f8c25119 100644
--- a/packages/adapter-netlify/package.json
+++ b/packages/adapter-netlify/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-netlify",
-	"version": "1.0.0-next.76",
+	"version": "1.0.0-next.77",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -33,9 +33,8 @@
 	},
 	"dependencies": {
 		"@iarna/toml": "^2.2.5",
-		"esbuild": "^0.14.48",
-		"set-cookie-parser": "^2.4.8",
-		"tiny-glob": "^0.2.9"
+		"esbuild": "^0.15.6",
+		"set-cookie-parser": "^2.4.8"
 	},
 	"devDependencies": {
 		"@netlify/functions": "^1.0.0",
diff --git a/packages/adapter-node/CHANGELOG.md b/packages/adapter-node/CHANGELOG.md
index 3672aabb2..929eeadae 100644
--- a/packages/adapter-node/CHANGELOG.md
+++ b/packages/adapter-node/CHANGELOG.md
@@ -1,5 +1,21 @@
 # @sveltejs/adapter-node
 
+## 1.0.0-next.90
+
+### Patch Changes
+
+- Update to esbuild 0.15 ([#6740](https://github.com/sveltejs/kit/pull/6740))
+
+* Improve performance by compressing in parallel ([#6710](https://github.com/sveltejs/kit/pull/6710))
+
+## 1.0.0-next.89
+
+### Patch Changes
+
+- [breaking] request creation cleanup ([#6681](https://github.com/sveltejs/kit/pull/6681))
+
+* [breaking] limit adapter-node request size ([#6684](https://github.com/sveltejs/kit/pull/6684))
+
 ## 1.0.0-next.88
 
 ### Patch Changes
diff --git a/packages/adapter-node/README.md b/packages/adapter-node/README.md
index 80dc3d2a9..9520f21d5 100644
--- a/packages/adapter-node/README.md
+++ b/packages/adapter-node/README.md
@@ -45,7 +45,7 @@ PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build
 
 ### `ADDRESS_HEADER` and `XFF_DEPTH`
 
-The [RequestEvent](https://kit.svelte.dev/docs/types#additional-types-requestevent) object passed to hooks and endpoints includes an `event.clientAddress` property representing the client's IP address. By default this is the connecting `remoteAddress`. If your server is behind one or more proxies (such as a load balancer), this value will contain the innermost proxy's IP address rather than the client's, so we need to specify an `ADDRESS_HEADER` to read the address from:
+The [RequestEvent](https://kit.svelte.dev/docs/types#sveltejs-kit-requestevent) object passed to hooks and endpoints includes an `event.clientAddress` property representing the client's IP address. By default this is the connecting `remoteAddress`. If your server is behind one or more proxies (such as a load balancer), this value will contain the innermost proxy's IP address rather than the client's, so we need to specify an `ADDRESS_HEADER` to read the address from:
 
 ```
 ADDRESS_HEADER=True-Client-IP node build
@@ -69,6 +69,10 @@ Instead, we read from the _right_, accounting for the number of trusted proxies.
 
 > If you need to read the left-most address instead (and don't care about spoofing) — for example, to offer a geolocation service, where it's more important for the IP address to be _real_ than _trusted_, you can do so by inspecting the `x-forwarded-for` header within your app.
 
+### `BODY_SIZE_LIMIT`
+
+The maximum request body size to accept in bytes including while streaming. Defaults to 512kb. You can disable this option with a value of 0 and implement a custom check in [`handle`](https://kit.svelte.dev/docs/hooks#server-hooks-handle) if you need something more advanced.
+
 ## Options
 
 The adapter can be configured with various options:
diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js
index 48a0ac5c3..cc3d0da78 100644
--- a/packages/adapter-node/index.js
+++ b/packages/adapter-node/index.js
@@ -32,8 +32,10 @@ export default function (opts = {}) {
 
 			if (precompress) {
 				builder.log.minor('Compressing assets');
-				await builder.compress(`${out}/client`);
-				await builder.compress(`${out}/prerendered`);
+				await Promise.all([
+					builder.compress(`${out}/client`),
+					builder.compress(`${out}/prerendered`)
+				]);
 			}
 
 			builder.log.minor('Building server');
diff --git a/packages/adapter-node/package.json b/packages/adapter-node/package.json
index d952096f1..656bbba56 100644
--- a/packages/adapter-node/package.json
+++ b/packages/adapter-node/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-node",
-	"version": "1.0.0-next.88",
+	"version": "1.0.0-next.90",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -45,6 +45,6 @@
 		"uvu": "^0.5.3"
 	},
 	"dependencies": {
-		"esbuild": "^0.14.48"
+		"esbuild": "^0.15.6"
 	}
 }
diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js
index b0951fccd..94f331f57 100644
--- a/packages/adapter-node/src/handler.js
+++ b/packages/adapter-node/src/handler.js
@@ -14,10 +14,10 @@ const server = new Server(manifest);
 await server.init({ env: process.env });
 const origin = env('ORIGIN', undefined);
 const xff_depth = parseInt(env('XFF_DEPTH', '1'));
-
 const address_header = env('ADDRESS_HEADER', '').toLowerCase();
 const protocol_header = env('PROTOCOL_HEADER', '').toLowerCase();
 const host_header = env('HOST_HEADER', 'host').toLowerCase();
+const body_size_limit = parseInt(env('BODY_SIZE_LIMIT', '524288'));
 
 const dir = path.dirname(fileURLToPath(import.meta.url));
 
@@ -49,7 +49,11 @@ const ssr = async (req, res) => {
 	let request;
 
 	try {
-		request = await getRequest(origin || get_origin(req.headers), req);
+		request = await getRequest({
+			base: origin || get_origin(req.headers),
+			request: req,
+			bodySizeLimit: body_size_limit
+		});
 	} catch (err) {
 		res.statusCode = err.status || 400;
 		res.end(err.reason || 'Invalid request body');
diff --git a/packages/adapter-static/CHANGELOG.md b/packages/adapter-static/CHANGELOG.md
index 1c27b28e9..120da9650 100644
--- a/packages/adapter-static/CHANGELOG.md
+++ b/packages/adapter-static/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-static
 
+## 1.0.0-next.43
+
+### Patch Changes
+
+- Improve performance by compressing in parallel ([#6710](https://github.com/sveltejs/kit/pull/6710))
+
 ## 1.0.0-next.42
 
 ### Patch Changes
diff --git a/packages/adapter-static/README.md b/packages/adapter-static/README.md
index a692e1bdf..bd90b5a2d 100644
--- a/packages/adapter-static/README.md
+++ b/packages/adapter-static/README.md
@@ -92,6 +92,8 @@ export default {
 };
 ```
 
+> How to configure this behaviour does however depend on your hosting solution and is not part of SvelteKit. It is recommended to search the host's documentation on how to redirect requests.
+
 When operating in SPA mode, you can omit the [`prerender`](https://kit.svelte.dev/docs/page-options#prerender) option from your root layout (or set it to `false`, its default value), and only pages that have the `prerender` option set will be prerendered at build time.
 
 SvelteKit will still crawl your app's entry points looking for prerenderable pages. If `svelte-kit build` fails because of pages that can't be loaded outside the browser, you can set `config.kit.prerender.entries` to `[]` to prevent this from happening. (Setting `config.kit.prerender.enabled` to `false` also has this effect, but would prevent the fallback page from being generated.)
diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js
index 8f2577e7a..03bdb8ab5 100644
--- a/packages/adapter-static/index.js
+++ b/packages/adapter-static/index.js
@@ -64,15 +64,11 @@ export default function (options) {
 			builder.writePrerendered(pages, { fallback });
 
 			if (precompress) {
+				builder.log.minor('Compressing assets and pages');
 				if (pages === assets) {
-					builder.log.minor('Compressing assets and pages');
 					await builder.compress(assets);
 				} else {
-					builder.log.minor('Compressing assets');
-					await builder.compress(assets);
-
-					builder.log.minor('Compressing pages');
-					await builder.compress(pages);
+					await Promise.all([builder.compress(assets), builder.compress(pages)]);
 				}
 			}
 
diff --git a/packages/adapter-static/package.json b/packages/adapter-static/package.json
index 8dd2269a6..b446f742d 100644
--- a/packages/adapter-static/package.json
+++ b/packages/adapter-static/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-static",
-	"version": "1.0.0-next.42",
+	"version": "1.0.0-next.43",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-vercel/CHANGELOG.md b/packages/adapter-vercel/CHANGELOG.md
index b934f1522..db6440c7b 100644
--- a/packages/adapter-vercel/CHANGELOG.md
+++ b/packages/adapter-vercel/CHANGELOG.md
@@ -1,5 +1,17 @@
 # @sveltejs/adapter-vercel
 
+## 1.0.0-next.74
+
+### Patch Changes
+
+- Update to esbuild 0.15 ([#6740](https://github.com/sveltejs/kit/pull/6740))
+
+## 1.0.0-next.73
+
+### Patch Changes
+
+- [breaking] request creation cleanup ([#6681](https://github.com/sveltejs/kit/pull/6681))
+
 ## 1.0.0-next.72
 
 ### Patch Changes
diff --git a/packages/adapter-vercel/README.md b/packages/adapter-vercel/README.md
index 03a2db919..c12ea6f82 100644
--- a/packages/adapter-vercel/README.md
+++ b/packages/adapter-vercel/README.md
@@ -38,7 +38,7 @@ export default {
 
 ### Vercel functions
 
-Vercel functions contained in the `/api` directory at the project's root will _not_ be included in the deployment — these should be implemented as [endpoints](https://kit.svelte.dev/docs/routing#endpoints) in your SvelteKit app.
+Vercel functions contained in the `/api` directory at the project's root will _not_ be included in the deployment — these should be implemented as [server endpoints](https://kit.svelte.dev/docs/routing#server) in your SvelteKit app.
 
 ### Node version
 
diff --git a/packages/adapter-vercel/files/serverless.js b/packages/adapter-vercel/files/serverless.js
index c647fd8b1..36fbae4c7 100644
--- a/packages/adapter-vercel/files/serverless.js
+++ b/packages/adapter-vercel/files/serverless.js
@@ -20,7 +20,7 @@ export default async (req, res) => {
 	let request;
 
 	try {
-		request = await getRequest(`https://${req.headers.host}`, req);
+		request = await getRequest({ base: `https://${req.headers.host}`, request: req });
 	} catch (err) {
 		res.statusCode = err.status || 400;
 		return res.end(err.reason || 'Invalid request body');
diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json
index f998aad6a..d36d75c92 100644
--- a/packages/adapter-vercel/package.json
+++ b/packages/adapter-vercel/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-vercel",
-	"version": "1.0.0-next.72",
+	"version": "1.0.0-next.74",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -29,7 +29,7 @@
 	},
 	"dependencies": {
 		"@vercel/nft": "^0.22.0",
-		"esbuild": "^0.14.48"
+		"esbuild": "^0.15.6"
 	},
 	"devDependencies": {
 		"@sveltejs/kit": "workspace:*",
diff --git a/packages/create-svelte/CHANGELOG.md b/packages/create-svelte/CHANGELOG.md
index 8ff37b4fa..3010c334c 100644
--- a/packages/create-svelte/CHANGELOG.md
+++ b/packages/create-svelte/CHANGELOG.md
@@ -1,5 +1,19 @@
 # create-svelte
 
+## 2.0.0-next.176
+
+### Patch Changes
+
+- SvelteKit 1.0 RC ([#6707](https://github.com/sveltejs/kit/pull/6707))
+
+## 2.0.0-next.175
+
+### Patch Changes
+
+- [breaking] update use:enhance usage ([#6697](https://github.com/sveltejs/kit/pull/6697))
+
+* [breaking] hooks file rename ([#6697](https://github.com/sveltejs/kit/pull/6697))
+
 ## 2.0.0-next.174
 
 ### Patch Changes
diff --git a/packages/create-svelte/README.md b/packages/create-svelte/README.md
index caeef9e55..99cf8e729 100644
--- a/packages/create-svelte/README.md
+++ b/packages/create-svelte/README.md
@@ -17,7 +17,7 @@ import { create } from 'create-svelte';
 
 await create('my-new-app', {
   name: 'my-new-app',
-  template: 'default', // or 'skeleton'
+  template: 'default', // or 'skeleton' or 'skeletonlib'
   types: 'checkjs', // or 'typescript' or null;
   prettier: false,
   eslint: false,
diff --git a/packages/create-svelte/bin.js b/packages/create-svelte/bin.js
index 96bdfb5e1..94c3dc529 100755
--- a/packages/create-svelte/bin.js
+++ b/packages/create-svelte/bin.js
@@ -10,7 +10,7 @@ import { dist } from './utils.js';
 const disclaimer = `
 ${bold(cyan('Welcome to SvelteKit!'))}
 
-${bold(red('This is beta software; expect bugs and missing features.'))}
+${bold(red('This is release candidate software; expect bugs and missing features.'))}
 
 Problems? Open an issue on ${cyan('https://github.com/sveltejs/kit/issues')} if none exists already.
 `;
diff --git a/packages/create-svelte/package.json b/packages/create-svelte/package.json
index b34284cc4..66c5cbd2f 100644
--- a/packages/create-svelte/package.json
+++ b/packages/create-svelte/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "create-svelte",
-	"version": "2.0.0-next.174",
+	"version": "2.0.0-next.176",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/create-svelte/templates/default/package.json b/packages/create-svelte/templates/default/package.json
index 360ec1230..4371c9c6e 100644
--- a/packages/create-svelte/templates/default/package.json
+++ b/packages/create-svelte/templates/default/package.json
@@ -13,7 +13,7 @@
 		"svelte": "^3.48.0",
 		"svelte-preprocess": "^4.10.6",
 		"typescript": "^4.8.2",
-		"vite": "3.1.0-beta.2"
+		"vite": "^3.1.0"
 	},
 	"type": "module",
 	"dependencies": {
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 5f5bfdc42..e4e472130 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,33 @@
 # @sveltejs/kit
 
+## 1.0.0-next.481
+
+### Patch Changes
+
+- [fix] docs links in generated and published types ([#6741](https://github.com/sveltejs/kit/pull/6741))
+
+## 1.0.0-next.480
+
+### Patch Changes
+
+- [breaking] make action passed to enhance function a URL instead of a string ([#6678](https://github.com/sveltejs/kit/pull/6678))
+
+## 1.0.0-next.479
+
+### Patch Changes
+
+- [breaking] request creation cleanup ([#6681](https://github.com/sveltejs/kit/pull/6681))
+
+* [breaking] limit adapter-node request size ([#6684](https://github.com/sveltejs/kit/pull/6684))
+
+## 1.0.0-next.478
+
+### Patch Changes
+
+- [breaking] hooks file renames; error shape defined through handleError ([#6675](https://github.com/sveltejs/kit/pull/6675))
+
+* [breaking] remove element property; enhance can only be used on form elements ([#6662](https://github.com/sveltejs/kit/pull/6662))
+
 ## 1.0.0-next.477
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 77626f6e5..5cc728230 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.477",
+	"version": "1.0.0-next.481",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/scripts/special-types/$env+dynamic+private.md b/packages/kit/scripts/special-types/$env+dynamic+private.md
index 675b1a96e..b4b63a9df 100644
--- a/packages/kit/scripts/special-types/$env+dynamic+private.md
+++ b/packages/kit/scripts/special-types/$env+dynamic+private.md
@@ -1,6 +1,6 @@
-This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/master/packages/adapter-node) (or running [`vite preview`](https://kit.svelte.dev/docs/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#kit-env-publicprefix).
+This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/master/packages/adapter-node) (or running [`vite preview`](https://kit.svelte.dev/docs/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env).
 
-This module cannot be imported into client-side code.
+This module cannot be imported into public-facing code.
 
 ```ts
 import { env } from '$env/dynamic/private';
diff --git a/packages/kit/scripts/special-types/$env+dynamic+public.md b/packages/kit/scripts/special-types/$env+dynamic+public.md
index 2c2ccdce9..b70626ba6 100644
--- a/packages/kit/scripts/special-types/$env+dynamic+public.md
+++ b/packages/kit/scripts/special-types/$env+dynamic+public.md
@@ -1,4 +1,4 @@
-Similar to [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), but only includes variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#kit-env-publicprefix) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
+Similar to [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), but only includes variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
 
 Note that public dynamic environment variables must all be sent from the server to the client, causing larger network requests — when possible, use `$env/static/public` instead.
 
diff --git a/packages/kit/scripts/special-types/$env+static+private.md b/packages/kit/scripts/special-types/$env+static+private.md
index cb490f888..cf2454a30 100644
--- a/packages/kit/scripts/special-types/$env+static+private.md
+++ b/packages/kit/scripts/special-types/$env+static+private.md
@@ -1,4 +1,4 @@
-Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), this module cannot be imported into client-side code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#kit-env-publicprefix).
+Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), this module cannot be imported into public-facing code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env).
 
 _Unlike_ [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), the values exported from this module are statically injected into your bundle at build time, enabling optimisations like dead code elimination.
 
diff --git a/packages/kit/scripts/special-types/$env+static+public.md b/packages/kit/scripts/special-types/$env+static+public.md
index 60fc224fa..2b55250f0 100644
--- a/packages/kit/scripts/special-types/$env+static+public.md
+++ b/packages/kit/scripts/special-types/$env+static+public.md
@@ -1,4 +1,4 @@
-Similar to [`$env/static/private`](https://kit.svelte.dev/docs/modules#$env-static-private), except that it only includes environment variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#kit-env-publicprefix) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
+Similar to [`$env/static/private`](https://kit.svelte.dev/docs/modules#$env-static-private), except that it only includes environment variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
 
 Values are replaced statically at build time.
 
diff --git a/packages/kit/scripts/special-types/$lib.md b/packages/kit/scripts/special-types/$lib.md
index 7365c4e6f..444a39cab 100644
--- a/packages/kit/scripts/special-types/$lib.md
+++ b/packages/kit/scripts/special-types/$lib.md
@@ -1,1 +1,5 @@
 This is a simple alias to `src/lib`, or whatever directory is specified as [`config.kit.files.lib`](https://kit.svelte.dev/docs/configuration#files). It allows you to access common components and utility modules without `../../../../` nonsense.
+
+#### `$lib/server`
+
+A subdirectory of `$lib`. SvelteKit will prevent you from importing any modules in `$lib/server` into public-facing code. See [server-only modules](/docs/server-only-modules).
diff --git a/packages/kit/src/exports/node/index.js b/packages/kit/src/exports/node/index.js
index 46026ce07..b73c5c91b 100644
--- a/packages/kit/src/exports/node/index.js
+++ b/packages/kit/src/exports/node/index.js
@@ -1,21 +1,38 @@
 import * as set_cookie_parser from 'set-cookie-parser';
 
-/** @param {import('http').IncomingMessage} req */
-function get_raw_body(req) {
+/**
+ * @param {import('http').IncomingMessage} req
+ * @param {number} [body_size_limit]
+ */
+function get_raw_body(req, body_size_limit) {
 	const h = req.headers;
 
 	if (!h['content-type']) {
 		return null;
 	}
 
-	const length = Number(h['content-length']);
+	const content_length = Number(h['content-length']);
 
 	// check if no request body
-	// https://github.com/jshttp/type-is/blob/c1f4388c71c8a01f79934e68f630ca4a15fffcd6/index.js#L81-L95
-	if (isNaN(length) && h['transfer-encoding'] == null) {
+	if (
+		(req.httpVersionMajor === 1 && isNaN(content_length) && h['transfer-encoding'] == null) ||
+		content_length === 0
+	) {
 		return null;
 	}
 
+	let length = content_length;
+
+	if (body_size_limit) {
+		if (!length) {
+			length = body_size_limit;
+		} else if (length > body_size_limit) {
+			throw new Error(
+				`Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.`
+			);
+		}
+	}
+
 	if (req.destroyed) {
 		const readable = new ReadableStream();
 		readable.cancel();
@@ -41,7 +58,13 @@ function get_raw_body(req) {
 
 				size += chunk.length;
 				if (size > length) {
-					controller.error(new Error('content-length exceeded'));
+					req.destroy(
+						new Error(
+							`request body size exceeded ${
+								content_length ? "'content-length'" : 'BODY_SIZE_LIMIT'
+							} of ${length}`
+						)
+					);
 					return;
 				}
 
@@ -65,9 +88,9 @@ function get_raw_body(req) {
 }
 
 /** @type {import('@sveltejs/kit/node').getRequest} */
-export async function getRequest(base, req) {
-	let headers = /** @type {Record<string, string>} */ (req.headers);
-	if (req.httpVersionMajor === 2) {
+export async function getRequest({ request, base, bodySizeLimit }) {
+	let headers = /** @type {Record<string, string>} */ (request.headers);
+	if (request.httpVersionMajor === 2) {
 		// we need to strip out the HTTP/2 pseudo-headers because node-fetch's
 		// Request implementation doesn't like them
 		// TODO is this still true with Node 18
@@ -78,10 +101,10 @@ export async function getRequest(base, req) {
 		delete headers[':scheme'];
 	}
 
-	return new Request(base + req.url, {
-		method: req.method,
+	return new Request(base + request.url, {
+		method: request.method,
 		headers,
-		body: get_raw_body(req)
+		body: get_raw_body(request, bodySizeLimit)
 	});
 }
 
diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js
index 5aef6af27..4a0e19e4a 100644
--- a/packages/kit/src/exports/vite/build/utils.js
+++ b/packages/kit/src/exports/vite/build/utils.js
@@ -147,7 +147,7 @@ export function get_default_build_config({ config, input, ssr, outDir }) {
  * @returns {string}
  */
 export function assets_base(config) {
-	return config.paths.assets + '/' || config.paths.base + '/' || './';
+	return (config.paths.assets || config.paths.base || '.') + '/';
 }
 
 const method_names = new Set(['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']);
diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js
index b8ecb67e0..3c28acb48 100644
--- a/packages/kit/src/exports/vite/dev/index.js
+++ b/packages/kit/src/exports/vite/dev/index.js
@@ -11,7 +11,7 @@ import { load_error_page, load_template } from '../../../core/config/index.js';
 import { SVELTE_KIT_ASSETS } from '../../../constants.js';
 import * as sync from '../../../core/sync/sync.js';
 import { get_mime_lookup, runtime_base, runtime_prefix } from '../../../core/utils.js';
-import { prevent_illegal_vite_imports } from '../utils.js';
+import { prevent_illegal_vite_imports } from '../graph_analysis/index.js';
 import { compact } from '../../../utils/array.js';
 import { normalizePath } from 'vite';
 
@@ -391,10 +391,13 @@ export async function dev(vite, vite_config, svelte_config) {
 				let request;
 
 				try {
-					request = await getRequest(base, req);
+					request = await getRequest({
+						base,
+						request: req
+					});
 				} catch (/** @type {any} */ err) {
 					res.statusCode = err.status || 400;
-					return res.end(err.reason || 'Invalid request body');
+					return res.end(err.message || 'Invalid request body');
 				}
 
 				const template = load_template(cwd, svelte_config);
diff --git a/packages/kit/src/exports/vite/graph_analysis/index.js b/packages/kit/src/exports/vite/graph_analysis/index.js
new file mode 100644
index 000000000..6c4050326
--- /dev/null
+++ b/packages/kit/src/exports/vite/graph_analysis/index.js
@@ -0,0 +1,277 @@
+import path from 'path';
+import { normalizePath } from 'vite';
+import { remove_query_from_id, get_module_types } from './utils.js';
+
+/** @typedef {import('./types').ImportGraph} ImportGraph */
+
+const CWD_ID = normalizePath(process.cwd());
+const NODE_MODULES_ID = normalizePath(path.resolve(process.cwd(), 'node_modules'));
+const ILLEGAL_IMPORTS = new Set([
+	'/@id/__x00__$env/dynamic/private', //dev
+	'\0$env/dynamic/private', // prod
+	'/@id/__x00__$env/static/private', // dev
+	'\0$env/static/private' // prod
+]);
+const ILLEGAL_MODULE_NAME_PATTERN = /.*\.server\..+/;
+
+export class IllegalModuleGuard {
+	/** @type {string} */
+	#lib_dir;
+
+	/** @type {string} */
+	#server_dir;
+
+	/** @type {Array<ImportGraph>} */
+	#chain = [];
+
+	/**
+	 * @param {string} lib_dir
+	 */
+	constructor(lib_dir) {
+		this.#lib_dir = normalizePath(lib_dir);
+		this.#server_dir = normalizePath(path.resolve(lib_dir, 'server'));
+	}
+
+	/**
+	 * Assert that a node imports no illegal modules.
+	 * @param {ImportGraph} node
+	 * @returns {void}
+	 */
+	assert_legal(node) {
+		this.#chain.push(node);
+		for (const child of node.children) {
+			if (this.#is_illegal(child.id)) {
+				this.#chain.push(child);
+				const error = this.#format_illegal_import_chain(this.#chain);
+				this.#chain = []; // Reset the chain in case we want to reuse this guard
+				throw new Error(error);
+			}
+			this.assert_legal(child);
+		}
+		this.#chain.pop();
+	}
+
+	/**
+	 * `true` if the provided ID represents a server-only module, else `false`.
+	 * @param {string} module_id
+	 * @returns {boolean}
+	 */
+	#is_illegal(module_id) {
+		if (this.#is_kit_illegal(module_id) || this.#is_user_illegal(module_id)) return true;
+		return false;
+	}
+
+	/**
+	 * `true` if the provided ID represents a Kit-defined server-only module, else `false`.
+	 * @param {string} module_id
+	 * @returns {boolean}
+	 */
+	#is_kit_illegal(module_id) {
+		return ILLEGAL_IMPORTS.has(module_id);
+	}
+
+	/**
+	 * `true` if the provided ID represents a user-defined server-only module, else `false`.
+	 * @param {string} module_id
+	 * @returns {boolean}
+	 */
+	#is_user_illegal(module_id) {
+		if (module_id.startsWith(this.#server_dir)) return true;
+
+		// files outside the project root are ignored
+		if (!module_id.startsWith(CWD_ID)) return false;
+
+		// so are files inside node_modules
+		if (module_id.startsWith(NODE_MODULES_ID)) return false;
+
+		return ILLEGAL_MODULE_NAME_PATTERN.test(path.basename(module_id));
+	}
+
+	/**
+	 * @param {string} str
+	 * @param {number} times
+	 */
+	#repeat(str, times) {
+		return new Array(times + 1).join(str);
+	}
+
+	/**
+	 * Create a formatted error for an illegal import.
+	 * @param {Array<ImportGraph>} stack
+	 */
+	#format_illegal_import_chain(stack) {
+		const dev_virtual_prefix = '/@id/__x00__';
+		const prod_virtual_prefix = '\0';
+
+		stack = stack.map((graph) => {
+			if (graph.id.startsWith(dev_virtual_prefix)) {
+				return { ...graph, id: graph.id.replace(dev_virtual_prefix, '') };
+			}
+			if (graph.id.startsWith(prod_virtual_prefix)) {
+				return { ...graph, id: graph.id.replace(prod_virtual_prefix, '') };
+			}
+			if (graph.id.startsWith(this.#lib_dir)) {
+				return { ...graph, id: graph.id.replace(this.#lib_dir, '$lib') };
+			}
+
+			return { ...graph, id: path.relative(process.cwd(), graph.id) };
+		});
+
+		const pyramid = stack
+			.map(
+				(file, i) =>
+					`${this.#repeat(' ', i * 2)}- ${file.id} ${
+						file.dynamic ? '(imported by parent dynamically)' : ''
+					}`
+			)
+			.join('\n');
+
+		return `Cannot import ${stack.at(-1)?.id} into public-facing code:\n${pyramid}`;
+	}
+}
+
+/** @implements {ImportGraph} */
+export class RollupImportGraph {
+	/** @type {(id: string) => import('rollup').ModuleInfo | null} */
+	#node_getter;
+
+	/** @type {import('rollup').ModuleInfo} */
+	#module_info;
+
+	/** @type {string} */
+	id;
+
+	/** @type {boolean} */
+	dynamic;
+
+	/** @type {Set<string>} */
+	#seen;
+
+	/**
+	 * @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
+	 * @param {import('rollup').ModuleInfo} node
+	 */
+	constructor(node_getter, node) {
+		this.#node_getter = node_getter;
+		this.#module_info = node;
+		this.id = remove_query_from_id(normalizePath(node.id));
+		this.dynamic = false;
+		this.#seen = new Set();
+	}
+
+	/**
+	 * @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
+	 * @param {import('rollup').ModuleInfo} node
+	 * @param {boolean} dynamic
+	 * @param {Set<string>} seen;
+	 * @returns {RollupImportGraph}
+	 */
+	static #new_internal(node_getter, node, dynamic, seen) {
+		const instance = new RollupImportGraph(node_getter, node);
+		instance.dynamic = dynamic;
+		instance.#seen = seen;
+		return instance;
+	}
+
+	get children() {
+		return this.#children();
+	}
+
+	*#children() {
+		if (this.#seen.has(this.id)) return;
+		this.#seen.add(this.id);
+		for (const id of this.#module_info.importedIds) {
+			const child = this.#node_getter(id);
+			if (child === null) return;
+			yield RollupImportGraph.#new_internal(this.#node_getter, child, false, this.#seen);
+		}
+		for (const id of this.#module_info.dynamicallyImportedIds) {
+			const child = this.#node_getter(id);
+			if (child === null) return;
+			yield RollupImportGraph.#new_internal(this.#node_getter, child, true, this.#seen);
+		}
+	}
+}
+
+/** @implements {ImportGraph} */
+export class ViteImportGraph {
+	/** @type {Set<string>} */
+	#module_types;
+
+	/** @type {import('vite').ModuleNode} */
+	#module_info;
+
+	/** @type {string} */
+	id;
+
+	/** @type {Set<string>} */
+	#seen;
+
+	/**
+	 * @param {Set<string>} module_types Module types to analyze, eg '.js', '.ts', etc.
+	 * @param {import('vite').ModuleNode} node
+	 */
+	constructor(module_types, node) {
+		this.#module_types = module_types;
+		this.#module_info = node;
+		this.id = remove_query_from_id(normalizePath(node.id ?? ''));
+		this.#seen = new Set();
+	}
+
+	/**
+	 * @param {Set<string>} module_types Module types to analyze, eg '.js', '.ts', etc.
+	 * @param {import('vite').ModuleNode} node
+	 * @param {Set<string>} seen
+	 * @returns {ViteImportGraph}
+	 */
+	static #new_internal(module_types, node, seen) {
+		const instance = new ViteImportGraph(module_types, node);
+		instance.#seen = seen;
+		return instance;
+	}
+
+	get dynamic() {
+		return false;
+	}
+
+	get children() {
+		return this.#children();
+	}
+
+	*#children() {
+		if (this.#seen.has(this.id)) return;
+		this.#seen.add(this.id);
+		for (const child of this.#module_info.importedModules) {
+			if (!this.#module_types.has(path.extname(this.id))) {
+				continue;
+			}
+			yield ViteImportGraph.#new_internal(this.#module_types, child, this.#seen);
+		}
+	}
+}
+
+/**
+ * Throw an error if a private module is imported from a client-side node.
+ * @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
+ * @param {import('rollup').ModuleInfo} node
+ * @param {string} lib_dir
+ * @returns {void}
+ */
+export function prevent_illegal_rollup_imports(node_getter, node, lib_dir) {
+	const graph = new RollupImportGraph(node_getter, node);
+	const guard = new IllegalModuleGuard(lib_dir);
+	guard.assert_legal(graph);
+}
+
+/**
+ * Throw an error if a private module is imported from a client-side node.
+ * @param {import('vite').ModuleNode} node
+ * @param {string} lib_dir
+ * @param {Iterable<string>} module_types File extensions to analyze in addition to the defaults: `.ts`, `.js`, etc.
+ * @returns {void}
+ */
+export function prevent_illegal_vite_imports(node, lib_dir, module_types) {
+	const graph = new ViteImportGraph(get_module_types(module_types), node);
+	const guard = new IllegalModuleGuard(lib_dir);
+	guard.assert_legal(graph);
+}
diff --git a/packages/kit/src/exports/vite/graph_analysis/index.spec.js b/packages/kit/src/exports/vite/graph_analysis/index.spec.js
new file mode 100644
index 000000000..1b00de5a8
--- /dev/null
+++ b/packages/kit/src/exports/vite/graph_analysis/index.spec.js
@@ -0,0 +1,191 @@
+import { describe } from '../../../utils/unit_test.js';
+import * as assert from 'uvu/assert';
+import { IllegalModuleGuard } from './index.js';
+import path from 'path';
+import { normalizePath } from 'vite';
+
+const CWD = process.cwd();
+const FAKE_LIB_DIR = normalizePath(path.join(CWD, 'lib'));
+const DEV_VIRTUAL_DYNAMIC_ID = '/@id/__x00__$env/dynamic/private';
+const PROD_VIRTUAL_DYNAMIC_ID = '\0$env/dynamic/private';
+const DEV_VIRTUAL_STATIC_ID = '/@id/__x00__$env/static/private';
+const PROD_VIRTUAL_STATIC_ID = '\0$env/static/private';
+const USER_SERVER_ID = normalizePath(path.join(FAKE_LIB_DIR, 'test.server.js'));
+const USER_SERVER_ID_NODE_MODULES = normalizePath(path.join(CWD, 'node_modules', 'test.server.js'));
+const USER_SERVER_ID_OUTSIDE_ROOT = normalizePath(path.join(CWD, '..', 'test.server.js'));
+const USER_SERVER_FOLDER_ID = normalizePath(path.join(FAKE_LIB_DIR, '/server/some/nested/path.js'));
+
+/**
+ * @template {any} T
+ * @param {Array<T>} arr
+ * @returns {Generator<T>}
+ */
+function* generator_from_array(arr) {
+	for (const item of arr) {
+		yield item;
+	}
+}
+
+/**
+ * @param {Array<import('./types').ImportGraph>} nodes_to_insert
+ * @returns {import('./types').ImportGraph}
+ */
+function get_module_graph(...nodes_to_insert) {
+	return {
+		id: 'test.svelte',
+		dynamic: false,
+		children: generator_from_array([
+			{
+				id: 'fine.js',
+				dynamic: false,
+				children: generator_from_array([
+					{
+						id: 'also_fine.js',
+						dynamic: false,
+						children: generator_from_array([
+							{
+								id: 'erstwhile.css',
+								dynamic: false,
+								children: generator_from_array([])
+							},
+							{
+								id: 'gruntled.js',
+								dynamic: false,
+								children: generator_from_array([])
+							}
+						])
+					},
+					{
+						id: 'somewhat_neat.js',
+						dynamic: false,
+						children: generator_from_array([])
+					},
+					{
+						id: 'blah.ts',
+						dynamic: false,
+						children: generator_from_array([])
+					}
+				])
+			},
+			{
+				id: 'something.svelte',
+				dynamic: false,
+				children: generator_from_array(nodes_to_insert)
+			},
+			{
+				id: 'im_not_creative.hamburger',
+				dynamic: false,
+				children: generator_from_array([])
+			}
+		])
+	};
+}
+
+describe('IllegalImportGuard', (test) => {
+	const guard = new IllegalModuleGuard(FAKE_LIB_DIR);
+
+	test('assert succeeds for a graph with no illegal imports', () => {
+		assert.not.throws(() => guard.assert_legal(get_module_graph()));
+	});
+
+	test('assert throws an error when importing $env/static/private in dev', () => {
+		const module_graph = get_module_graph({
+			id: DEV_VIRTUAL_STATIC_ID,
+			dynamic: false,
+			children: generator_from_array([])
+		});
+		assert.throws(
+			() => guard.assert_legal(module_graph),
+			/.*Cannot import \$env\/static\/private into public-facing code:.*/gs
+		);
+	});
+
+	test('assert throws an error when importing $env/static/private in prod', () => {
+		const module_graph = get_module_graph({
+			id: PROD_VIRTUAL_STATIC_ID,
+			dynamic: false,
+			children: generator_from_array([])
+		});
+		assert.throws(
+			() => guard.assert_legal(module_graph),
+			/.*Cannot import \$env\/static\/private into public-facing code:.*/gs
+		);
+	});
+
+	test('assert throws an error when importing $env/dynamic/private in dev', () => {
+		const module_graph = get_module_graph({
+			id: DEV_VIRTUAL_DYNAMIC_ID,
+			dynamic: false,
+			children: generator_from_array([])
+		});
+		assert.throws(
+			() => guard.assert_legal(module_graph),
+			/.*Cannot import \$env\/dynamic\/private into public-facing code:.*/gs
+		);
+	});
+
+	test('assert throws an error when importing $env/dynamic/private in prod', () => {
+		const module_graph = get_module_graph({
+			id: PROD_VIRTUAL_DYNAMIC_ID,
+			dynamic: false,
+			children: generator_from_array([])
+		});
+		assert.throws(
+			() => guard.assert_legal(module_graph),
+			/.*Cannot import \$env\/dynamic\/private into public-facing code:.*/gs
+		);
+	});
+
+	test('assert throws an error when importing a single server-only module', () => {
+		const module_graph = get_module_graph({
+			id: USER_SERVER_ID,
+			dynamic: false,
+			children: generator_from_array([])
+		});
+
+		assert.throws(
+			() => guard.assert_legal(module_graph),
+			/.*Cannot import \$lib\/test.server.js into public-facing code:.*/gs
+		);
+	});
+
+	test('assert throws an error when importing a module in the server-only folder', () => {
+		const module_graph = get_module_graph({
+			id: USER_SERVER_FOLDER_ID,
+			dynamic: false,
+			children: generator_from_array([])
+		});
+
+		assert.throws(
+			() => guard.assert_legal(module_graph),
+			/.*Cannot import \$lib\/server\/some\/nested\/path.js into public-facing code:.*/gs
+		);
+	});
+
+	test('assert ignores illegal server-only modules in node_modules', () => {
+		const module_graph = get_module_graph({
+			id: USER_SERVER_ID_NODE_MODULES,
+			dynamic: false,
+			children: generator_from_array([])
+		});
+
+		assert.not.throws(() => guard.assert_legal(module_graph));
+	});
+
+	test('assert ignores illegal server-only modules outside the project root', () => {
+		const module_graph = get_module_graph({
+			id: USER_SERVER_ID_OUTSIDE_ROOT,
+			dynamic: false,
+			children: generator_from_array([])
+		});
+
+		assert.not.throws(() => guard.assert_legal(module_graph));
+	});
+});
+
+/*
+We don't have a great way to mock Vite and Rollup's implementations of module graphs, so unit testing
+ViteImportGraph and RollupImportGraph is kind of an exercise in "code coverage hubris" -- they're covered by
+the integration tests, where Vite and Rollup can provide a useful graph implementation. If, in the future, we can find
+a reason to unit test them, we can add those below.
+*/
diff --git a/packages/kit/src/exports/vite/graph_analysis/types.d.ts b/packages/kit/src/exports/vite/graph_analysis/types.d.ts
new file mode 100644
index 000000000..1239fec43
--- /dev/null
+++ b/packages/kit/src/exports/vite/graph_analysis/types.d.ts
@@ -0,0 +1,5 @@
+export interface ImportGraph {
+	readonly id: string;
+	readonly dynamic: boolean;
+	readonly children: Generator<ImportGraph>;
+}
diff --git a/packages/kit/src/exports/vite/graph_analysis/utils.js b/packages/kit/src/exports/vite/graph_analysis/utils.js
new file mode 100644
index 000000000..f2caa3e04
--- /dev/null
+++ b/packages/kit/src/exports/vite/graph_analysis/utils.js
@@ -0,0 +1,30 @@
+const query_pattern = /\?.*$/s;
+
+/** @param {string} path */
+export function remove_query_from_id(path) {
+	return path.replace(query_pattern, '');
+}
+
+/**
+ * Vite does some weird things with import trees in dev
+ * for example, a Tailwind app.css will appear to import
+ * every file in the project. This isn't a problem for
+ * Rollup during build.
+ * @param {Iterable<string>} config_module_types
+ */
+export const get_module_types = (config_module_types) => {
+	return new Set([
+		'',
+		'.ts',
+		'.js',
+		'.svelte',
+		'.mts',
+		'.mjs',
+		'.cts',
+		'.cjs',
+		'.svelte.md',
+		'.svx',
+		'.md',
+		...config_module_types
+	]);
+};
diff --git a/packages/kit/src/exports/vite/graph_analysis/utils.spec.js b/packages/kit/src/exports/vite/graph_analysis/utils.spec.js
new file mode 100644
index 000000000..7aff1b999
--- /dev/null
+++ b/packages/kit/src/exports/vite/graph_analysis/utils.spec.js
@@ -0,0 +1,58 @@
+import { describe } from '../../../utils/unit_test.js';
+import * as assert from 'uvu/assert';
+import { remove_query_from_id, get_module_types } from './utils.js';
+
+describe('remove_query_string_from_path', (test) => {
+	const module_ids = [
+		'$env/static/private',
+		'some-normal-js-module.js',
+		'c:\\\\some\\stupid\\windows\\path.js',
+		'/some/normal/linux/path.js'
+	];
+	const query_module_ids = module_ids.map((module_id) => `${module_id}?hello=world,something=else`);
+
+	test('does nothing to valid IDs', () => {
+		module_ids.forEach((id) => {
+			const query_stringless = remove_query_from_id(id);
+			assert.equal(query_stringless, id);
+		});
+	});
+
+	test('removes querystring from paths with querystrings at the end', () => {
+		query_module_ids.forEach((id, i) => {
+			const query_stringless = remove_query_from_id(id);
+			assert.equal(query_stringless, module_ids[i]);
+		});
+	});
+});
+
+describe('get_module_types', (test) => {
+	const base_expected_extensions = [
+		'',
+		'.ts',
+		'.js',
+		'.svelte',
+		'.mts',
+		'.mjs',
+		'.cts',
+		'.cjs',
+		'.svelte.md',
+		'.svx',
+		'.md'
+	];
+
+	test('returns correct base extensions', () => {
+		const module_types = get_module_types([]);
+		base_expected_extensions.forEach((extension) => {
+			assert.equal(module_types.has(extension), true);
+		});
+	});
+
+	test('correctly extends base extensions', () => {
+		const additional_extensions = ['.foo', '.bar', '.baz'];
+		const module_types = get_module_types(additional_extensions);
+		base_expected_extensions.concat(additional_extensions).forEach((extension) => {
+			assert.equal(module_types.has(extension), true);
+		});
+	});
+});
diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js
index b7eda46b4..c6b99a815 100644
--- a/packages/kit/src/exports/vite/index.js
+++ b/packages/kit/src/exports/vite/index.js
@@ -14,7 +14,8 @@ import { generate_manifest } from '../../core/generate_manifest/index.js';
 import { runtime_directory, logger } from '../../core/utils.js';
 import { find_deps, get_default_build_config } from './build/utils.js';
 import { preview } from './preview/index.js';
-import { get_aliases, prevent_illegal_rollup_imports, get_env } from './utils.js';
+import { get_aliases, get_env } from './utils.js';
+import { prevent_illegal_rollup_imports } from './graph_analysis/index.js';
 import { fileURLToPath } from 'node:url';
 import { create_static_module, create_dynamic_module } from '../../core/env.js';
 
diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js
index 9f1a80298..55a59105b 100644
--- a/packages/kit/src/exports/vite/preview/index.js
+++ b/packages/kit/src/exports/vite/preview/index.js
@@ -131,10 +131,13 @@ export async function preview(vite, vite_config, svelte_config) {
 			let request;
 
 			try {
-				request = await getRequest(`${protocol}://${host}`, req);
+				request = await getRequest({
+					base: `${protocol}://${host}`,
+					request: req
+				});
 			} catch (/** @type {any} */ err) {
 				res.statusCode = err.status || 400;
-				return res.end(err.reason || 'Invalid request body');
+				return res.end(err.message || 'Invalid request body');
 			}
 
 			setResponse(
diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js
index fc4ff2159..cfbafa7b6 100644
--- a/packages/kit/src/exports/vite/utils.js
+++ b/packages/kit/src/exports/vite/utils.js
@@ -1,232 +1,8 @@
 import path from 'path';
-import { loadConfigFromFile, loadEnv, normalizePath } from 'vite';
+import { loadConfigFromFile, loadEnv } from 'vite';
 import { runtime_directory } from '../../core/utils.js';
 import { posixify } from '../../utils/filesystem.js';
 
-class IllegalModuleGuard {
-	/** @type {string} */
-	#lib_dir;
-
-	/** @type {string} */
-	#server_dir;
-
-	/** @type {string} */
-	#node_modules_dir = normalizePath(path.resolve(process.cwd(), 'node_modules'));
-
-	/** @type {string} */
-	#cwd = normalizePath(process.cwd());
-
-	/** @type {Set<string>} */
-	#illegal_imports = new Set([
-		'/@id/__x00__$env/dynamic/private', //dev
-		'\0$env/dynamic/private', // prod
-		'/@id/__x00__$env/static/private', // dev
-		'\0$env/static/private' // prod
-	]);
-
-	/** @type {Array<import('types').ImportNode>} */
-	#chain = [];
-
-	/**
-	 * @param {string} lib_dir
-	 */
-	constructor(lib_dir) {
-		this.#lib_dir = normalizePath(lib_dir);
-		this.#server_dir = normalizePath(path.resolve(lib_dir, 'server'));
-	}
-
-	/**
-	 * @param {import('types').ImportNode} node
-	 * @returns {void}
-	 */
-	assert_legal(node) {
-		this.#chain.push(node);
-		for (const child of node.children) {
-			if (this.#is_illegal(child.name)) {
-				this.#chain.push(child);
-				throw new Error(this.#format_illegal_import_chain(this.#chain));
-			}
-			this.assert_legal(child);
-		}
-		this.#chain.pop();
-	}
-
-	/**
-	 * `true` if the provided ID represents a server-only module, else `false`.
-	 * @param {string} module_id
-	 * @returns {boolean}
-	 */
-	#is_illegal(module_id) {
-		if (this.#is_kit_illegal(module_id) || this.#is_user_illegal(module_id)) return true;
-		return false;
-	}
-
-	/**
-	 * `true` if the provided ID represents a Kit-defined server-only module, else `false`.
-	 * @param {string} module_id
-	 * @returns {boolean}
-	 */
-	#is_kit_illegal(module_id) {
-		return this.#illegal_imports.has(module_id);
-	}
-
-	/**
-	 * `true` if the provided ID represents a user-defined server-only module, else `false`.
-	 * @param {string} module_id
-	 * @returns {boolean}
-	 */
-	#is_user_illegal(module_id) {
-		if (module_id.startsWith(this.#server_dir)) return true;
-
-		// files outside the project root are ignored
-		if (!module_id.startsWith(this.#cwd)) return false;
-
-		// so are files inside node_modules
-		if (module_id.startsWith(this.#node_modules_dir)) return false;
-
-		return /.*\.server\..+/.test(path.basename(module_id));
-	}
-
-	/**
-	 * @param {string} str
-	 * @param {number} times
-	 */
-	#repeat(str, times) {
-		return new Array(times + 1).join(str);
-	}
-
-	/**
-	 * Create a formatted error for an illegal import.
-	 * @param {Array<{name: string, dynamic: boolean}>} stack
-	 */
-	#format_illegal_import_chain(stack) {
-		const dev_virtual_prefix = '/@id/__x00__';
-		const prod_virtual_prefix = '\0';
-
-		stack = stack.map((file) => {
-			if (file.name.startsWith(dev_virtual_prefix)) {
-				return { ...file, name: file.name.replace(dev_virtual_prefix, '') };
-			}
-			if (file.name.startsWith(prod_virtual_prefix)) {
-				return { ...file, name: file.name.replace(prod_virtual_prefix, '') };
-			}
-			if (file.name.startsWith(this.#lib_dir)) {
-				return { ...file, name: file.name.replace(this.#lib_dir, '$lib') };
-			}
-
-			return { ...file, name: path.relative(process.cwd(), file.name) };
-		});
-
-		const pyramid = stack
-			.map(
-				(file, i) =>
-					`${this.#repeat(' ', i * 2)}- ${file.name} ${
-						file.dynamic ? '(imported by parent dynamically)' : ''
-					}`
-			)
-			.join('\n');
-
-		return `Cannot import ${stack.at(-1)?.name} into client-side code:\n${pyramid}`;
-	}
-}
-
-class RollupImportGraph {
-	/** @type {(id: string) => import('rollup').ModuleInfo | null} */
-	#node_getter;
-
-	/** @type {import('rollup').ModuleInfo} */
-	#module_info;
-
-	/** @type {string} */
-	name;
-
-	/** @type {boolean} */
-	dynamic;
-
-	/** @type {Set<string>} */
-	#seen;
-
-	/**
-	 * @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
-	 * @param {import('rollup').ModuleInfo} node
-	 * @param {boolean} dynamic
-	 * @param {Set<string>} seen
-	 */
-	constructor(node_getter, node, dynamic = false, seen = new Set()) {
-		this.#node_getter = node_getter;
-		this.#module_info = node;
-		this.name = remove_query_from_path(normalizePath(node.id));
-		this.dynamic = dynamic;
-		this.#seen = seen;
-		void (/** @type {import('types').ImportNode} */ (this));
-	}
-
-	get children() {
-		return this.#children();
-	}
-
-	*#children() {
-		if (this.#seen.has(this.name)) return;
-		this.#seen.add(this.name);
-		for (const id of this.#module_info.importedIds) {
-			const child = this.#node_getter(id);
-			if (child === null) return;
-			yield new RollupImportGraph(this.#node_getter, child, false, this.#seen);
-		}
-		for (const id of this.#module_info.dynamicallyImportedIds) {
-			const child = this.#node_getter(id);
-			if (child === null) return;
-			yield new RollupImportGraph(this.#node_getter, child, true, this.#seen);
-		}
-	}
-}
-
-class ViteImportGraph {
-	/** @type {Set<string>} */
-	#module_types;
-
-	/** @type {import('vite').ModuleNode} */
-	#module_info;
-
-	/** @type {string} */
-	name;
-
-	/** @type {Set<string>} */
-	#seen;
-
-	/**
-	 * @param {Set<string>} module_types Module types to analyze, eg '.js', '.ts', etc.
-	 * @param {import('vite').ModuleNode} node
-	 * @param {Set<string>} seen
-	 */
-	constructor(module_types, node, seen = new Set()) {
-		this.#module_types = module_types;
-		this.#module_info = node;
-		this.name = remove_query_from_path(normalizePath(node.id ?? ''));
-		this.#seen = seen;
-		void (/** @type {import('types').ImportNode} */ (this));
-	}
-
-	get dynamic() {
-		return false;
-	}
-
-	get children() {
-		return this.#children();
-	}
-
-	*#children() {
-		if (this.#seen.has(this.name)) return;
-		this.#seen.add(this.name);
-		for (const child of this.#module_info.importedModules) {
-			if (!this.#module_types.has(path.extname(this.name))) {
-				continue;
-			}
-			yield new ViteImportGraph(this.#module_types, child, this.#seen);
-		}
-	}
-}
-
 /**
  * @param {import('vite').ResolvedConfig} config
  * @param {import('vite').ConfigEnv} config_env
@@ -379,60 +155,3 @@ export function get_env(env_config, mode) {
 		private: Object.fromEntries(entries.filter(([k]) => !k.startsWith(env_config.publicPrefix)))
 	};
 }
-
-const query_pattern = /\?.*$/s;
-
-/** @param {string} path */
-function remove_query_from_path(path) {
-	return path.replace(query_pattern, '');
-}
-
-/**
- * Vite does some weird things with import trees in dev
- * for example, a Tailwind app.css will appear to import
- * every file in the project. This isn't a problem for
- * Rollup during build.
- * @param {Iterable<string>} config_module_types
- */
-const get_module_types = (config_module_types) => {
-	return new Set([
-		'',
-		'.ts',
-		'.js',
-		'.svelte',
-		'.mts',
-		'.mjs',
-		'.cts',
-		'.cjs',
-		'.svelte.md',
-		'.svx',
-		'.md',
-		...config_module_types
-	]);
-};
-
-/**
- * Throw an error if a private module is imported from a client-side node.
- * @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
- * @param {import('rollup').ModuleInfo} node
- * @param {string} lib_dir
- * @returns {void}
- */
-export function prevent_illegal_rollup_imports(node_getter, node, lib_dir) {
-	const graph = new RollupImportGraph(node_getter, node);
-	const guard = new IllegalModuleGuard(lib_dir);
-	guard.assert_legal(graph);
-}
-
-/**
- * Throw an error if a private module is imported from a client-side node.
- * @param {import('vite').ModuleNode} node
- * @param {string} lib_dir
- * @param {Iterable<string>} module_types File extensions to analyze in addition to the defaults: `.ts`, `.js`, etc.
- * @returns {void}
- */
-export function prevent_illegal_vite_imports(node, lib_dir, module_types) {
-	const graph = new ViteImportGraph(get_module_types(module_types), node);
-	const guard = new IllegalModuleGuard(lib_dir);
-	guard.assert_legal(graph);
-}
diff --git a/packages/kit/src/exports/vite/utils.spec.js b/packages/kit/src/exports/vite/utils.spec.js
index 558d5d563..f2160386d 100644
--- a/packages/kit/src/exports/vite/utils.spec.js
+++ b/packages/kit/src/exports/vite/utils.spec.js
@@ -3,15 +3,7 @@ import { test } from 'uvu';
 import * as assert from 'uvu/assert';
 import { validate_config } from '../../core/config/index.js';
 import { posixify } from '../../utils/filesystem.js';
-import {
-	deep_merge,
-	get_aliases,
-	merge_vite_configs,
-	prevent_illegal_rollup_imports,
-	prevent_illegal_vite_imports
-} from './utils.js';
-
-const illegal_id = '/@id/__x00__$env/dynamic/private';
+import { deep_merge, get_aliases, merge_vite_configs } from './utils.js';
 
 test('basic test no conflicts', async () => {
 	const merged = deep_merge(
@@ -240,164 +232,4 @@ test('transform kit.alias to resolve.alias', () => {
 	]);
 });
 
-/** @typedef {{id: string, importedIds: Array<string>, dynamicallyImportedIds: Array<string> }} RollupNode */
-
-/** @type {(id: string) => RollupNode | null} */
-const rollup_node_getter = (id) => {
-	/** @type {{[key: string]: RollupNode}} */
-	const nodes = {
-		'/test/path1.js': {
-			id: '/test/path1.js',
-			importedIds: ['/test/path2.js', '/test/path3.js'],
-			dynamicallyImportedIds: ['/test/path4.js', '/test/path5.js']
-		},
-		'/test/path2.js': {
-			id: '/test/path2.js',
-			importedIds: ['/test/path3.js'],
-			dynamicallyImportedIds: ['/test/path5.js']
-		},
-		'/test/path3.js': {
-			id: '/test/path3.js',
-			importedIds: ['/test/path5.js'],
-			dynamicallyImportedIds: ['/test/path1.js']
-		},
-		'/test/path4.js': {
-			id: '/test/path4.js',
-			importedIds: ['/test/path5.js'],
-			dynamicallyImportedIds: ['/test/path3.js']
-		},
-		'/test/path5.js': {
-			id: '/test/path5.js',
-			importedIds: ['/test/path1.js'],
-			dynamicallyImportedIds: ['/test/path3.js']
-		},
-		'/bad/static.js': {
-			id: '/bad/static.js',
-			importedIds: ['/statically-imports/bad/module.js'],
-			dynamicallyImportedIds: ['/test/path1.js']
-		},
-		'/statically-imports/bad/module.js': {
-			id: '/statically-imports/bad/module.js',
-			importedIds: [illegal_id],
-			dynamicallyImportedIds: ['/test/path2.js']
-		},
-		'/bad/dynamic.js': {
-			id: '/bad/dynamic.js',
-			importedIds: ['/dynamically-imports/bad/module.js'],
-			dynamicallyImportedIds: ['/test/path1.js']
-		},
-		'/dynamically-imports/bad/module.js': {
-			id: '/dynamically-imports/bad/module.js',
-			importedIds: ['/test/path5.js'],
-			dynamicallyImportedIds: ['/test/path2.js', illegal_id]
-		},
-		[illegal_id]: {
-			id: illegal_id,
-			importedIds: [],
-			dynamicallyImportedIds: []
-		}
-	};
-	return nodes[id] ?? null;
-};
-
-const ok_rollup_node = rollup_node_getter('/test/path1.js');
-const bad_rollup_node_static = rollup_node_getter('/bad/static.js');
-const bad_rollup_node_dynamic = rollup_node_getter('/bad/dynamic.js');
-
-test('allows ok rollup imports', () => {
-	assert.not.throws(() => {
-		prevent_illegal_rollup_imports(
-			// @ts-expect-error
-			rollup_node_getter,
-			ok_rollup_node,
-			'should_not_match_anything'
-		);
-	});
-});
-
-test('does not allow bad static rollup imports', () => {
-	assert.throws(() => {
-		prevent_illegal_rollup_imports(
-			// @ts-expect-error
-			rollup_node_getter,
-			bad_rollup_node_static,
-			'should_not_match_anything'
-		);
-	});
-});
-
-test('does not allow bad dynamic rollup imports', () => {
-	assert.throws(() => {
-		prevent_illegal_rollup_imports(
-			// @ts-expect-error
-			rollup_node_getter,
-			bad_rollup_node_dynamic,
-			'should_not_match_anything'
-		);
-	});
-});
-
-/** @typedef {{id: string, importedModules: Set<ViteNode>}} ViteNode */
-
-/** @type {ViteNode} */
-const ok_vite_node = {
-	id: '/test/ok.js',
-	importedModules: new Set([
-		{
-			id: '/test/path1.js',
-			importedModules: new Set([
-				{
-					id: '/test/path2.js',
-					importedModules: new Set()
-				}
-			])
-		},
-		{ id: '/test/path3.js', importedModules: new Set() }
-	])
-};
-
-/** @type {ViteNode} */
-const bad_vite_node = {
-	id: '/test/bad-static.js',
-	importedModules: new Set([
-		{
-			id: '/test/path1.js',
-			importedModules: new Set([
-				{
-					id: '/test/path2.js',
-					importedModules: new Set([
-						{
-							id: illegal_id,
-							importedModules: new Set()
-						}
-					])
-				}
-			])
-		},
-		{ id: '/test/path3.js', importedModules: new Set() }
-	])
-};
-
-test('allows ok vite imports', () => {
-	assert.not.throws(() => {
-		prevent_illegal_vite_imports(
-			// @ts-expect-error
-			ok_vite_node,
-			'should_not_match_anything',
-			[]
-		);
-	});
-});
-
-test('does not allow bad static rollup imports', () => {
-	assert.throws(() => {
-		prevent_illegal_vite_imports(
-			// @ts-expect-error
-			bad_vite_node,
-			'should_not_match_anything',
-			[]
-		);
-	});
-});
-
 test.run();
diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js
index c137a08d7..6ab589caa 100644
--- a/packages/kit/src/runtime/app/forms.js
+++ b/packages/kit/src/runtime/app/forms.js
@@ -16,46 +16,46 @@ const ssr = import.meta.env.SSR;
 export const applyAction = ssr ? guard('applyAction') : client.apply_action;
 
 /** @type {import('$app/forms').enhance} */
-export function enhance(element, submit = () => {}) {
+export function enhance(form, submit = () => {}) {
 	/**
 	 * @param {{
-	 *   element: HTMLFormElement | HTMLButtonElement | HTMLInputElement;
-	 *   form: HTMLFormElement;
+	 *   action: URL;
 	 *   result: import('types').ActionResult;
 	 * }} opts
 	 */
-	const fallback_callback = async ({ element, form, result }) => {
+	const fallback_callback = async ({ action, result }) => {
 		if (result.type === 'success') {
 			await invalidateAll();
 		}
 
-		const action = element.formAction ?? form.action;
-
-		if (location.origin + location.pathname === action.split('?')[0]) {
+		if (location.origin + location.pathname === action.origin + action.pathname) {
 			applyAction(result);
 		}
 	};
 
-	const form =
-		element instanceof HTMLFormElement ? element : /** @type {HTMLFormElement} */ (element.form);
-	if (!form) throw new Error('Element is not associated with a form');
-
 	/** @param {SubmitEvent} event */
 	async function handle_submit(event) {
 		event.preventDefault();
 
-		const action = element.formAction ?? form.action;
+		const action = new URL(
+			// We can't do submitter.formAction directly because that property is always set
+			event.submitter?.hasAttribute('formaction')
+				? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction
+				: form.action
+		);
 
 		const data = new FormData(form);
+		const controller = new AbortController();
 
 		let cancelled = false;
 		const cancel = () => (cancelled = true);
 
 		const callback =
 			submit({
-				element,
-				data,
+				action,
 				cancel,
+				controller,
+				data,
 				form
 			}) ?? fallback_callback;
 		if (cancelled) return;
@@ -69,16 +69,18 @@ export function enhance(element, submit = () => {}) {
 				headers: {
 					accept: 'application/json'
 				},
-				body: data
+				body: data,
+				signal: controller.signal
 			});
 
 			result = await response.json();
 		} catch (error) {
+			if (/** @type {any} */ (error)?.name === 'AbortError') return;
 			result = { type: 'error', error };
 		}
 
 		callback({
-			element,
+			action,
 			data,
 			form,
 			// @ts-expect-error generic constraints stuff we don't care about
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 755f0d85c..09be6d707 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1506,7 +1506,7 @@ async function load_data(url, invalid) {
  * @returns {App.PageError}
  */
 function handle_error(error, event) {
-	return hooks.handleError({ error, event }) ?? /** @type {any} */ ({ error: 'Internal Error' });
+	return hooks.handleError({ error, event }) ?? /** @type {any} */ ({ message: 'Internal Error' });
 }
 
 // TODO remove for 1.0
diff --git a/packages/kit/src/utils/url.spec.js b/packages/kit/src/utils/url.spec.js
index 3d9603055..4c359b224 100644
--- a/packages/kit/src/utils/url.spec.js
+++ b/packages/kit/src/utils/url.spec.js
@@ -1,18 +1,7 @@
-import { suite } from 'uvu';
 import * as assert from 'uvu/assert';
+import { describe } from './unit_test.js';
 import { resolve, normalize_path, make_trackable, disable_search } from './url.js';
 
-/**
- *
- * @param {string} name
- * @param {(suite: import('uvu').Test<import('uvu').Context>) => void} fn
- */
-function describe(name, fn) {
-	const s = suite(name);
-	fn(s);
-	s.run();
-}
-
 describe('resolve', (test) => {
 	test('resolves a root-relative path', () => {
 		assert.equal(resolve('/a/b/c', '/x/y/z'), '/x/y/z');
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index e3ce20d11..026ce6c5e 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -96,20 +96,19 @@ declare module '$app/forms' {
 	import type { ActionResult } from '@sveltejs/kit';
 
 	export type SubmitFunction<
-		Element extends HTMLFormElement | HTMLInputElement | HTMLButtonElement = HTMLFormElement,
 		Success extends Record<string, unknown> | undefined = Record<string, any>,
 		Invalid extends Record<string, unknown> | undefined = Record<string, any>
 	> = (input: {
+		action: URL;
 		data: FormData;
 		form: HTMLFormElement;
-		element: Element;
+		controller: AbortController;
 		cancel: () => void;
 	}) =>
 		| void
 		| ((opts: {
-				data: FormData;
 				form: HTMLFormElement;
-				element: Element;
+				action: URL;
 				result: ActionResult<Success, Invalid>;
 		  }) => void);
 
@@ -119,14 +118,14 @@ declare module '$app/forms' {
 	 * @param options Callbacks for different states of the form lifecycle
 	 */
 	export function enhance<
-		Element extends HTMLFormElement | HTMLInputElement | HTMLButtonElement = HTMLFormElement,
 		Success extends Record<string, unknown> | undefined = Record<string, any>,
 		Invalid extends Record<string, unknown> | undefined = Record<string, any>
 	>(
-		element: Element,
+		form: HTMLFormElement,
 		/**
-		 * Called upon submission with the given FormData.
+		 * Called upon submission with the given FormData and the `action` that should be triggered.
 		 * If `cancel` is called, the form will not be submitted.
+		 * You can use the abort `controller` to cancel the submission in case another one starts.
 		 * If a function is returned, that function is called with the response from the server.
 		 * If nothing is returned, the fallback will be used.
 		 *
@@ -137,7 +136,7 @@ declare module '$app/forms' {
 		 * - redirects in case of a redirect response
 		 * - redirects to the nearest error page in case of an unexpected error
 		 */
-		submit?: SubmitFunction<Element, Success, Invalid>
+		submit?: SubmitFunction<Success, Invalid>
 	): { destroy: () => void };
 
 	/**
@@ -409,10 +408,11 @@ declare module '@sveltejs/kit/node/polyfills' {
  * Utilities used by adapters for Node-like environments.
  */
 declare module '@sveltejs/kit/node' {
-	export function getRequest(
-		base: string,
-		request: import('http').IncomingMessage
-	): Promise<Request>;
+	export function getRequest(opts: {
+		base: string;
+		request: import('http').IncomingMessage;
+		bodySizeLimit?: number;
+	}): Promise<Request>;
 	export function setResponse(res: import('http').ServerResponse, response: Response): void;
 }
 
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 6b3d40651..fedab7854 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -16,7 +16,7 @@ import {
 	TrailingSlash
 } from './private.js';
 import { SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js';
-import { HttpError, Redirect, ValidationError } from '../src/runtime/control.js';
+import { HttpError, Redirect } from '../src/runtime/control.js';
 
 export { PrerenderOption } from './private.js';
 
@@ -39,6 +39,12 @@ export type AwaitedActions<T extends Record<string, (...args: any) => any>> = {
 	[Key in keyof T]: UnpackValidationError<Awaited<ReturnType<T[Key]>>>;
 }[keyof T];
 
+// Needs to be here, else ActionData will be resolved to unknown - probably because of "d.ts file imports .js file" in combination with allowJs
+interface ValidationError<T extends Record<string, unknown> | undefined = undefined> {
+	status: number;
+	data: T;
+}
+
 type UnpackValidationError<T> = T extends ValidationError<infer X> ? X : T;
 
 export interface Builder {
@@ -292,7 +298,7 @@ export interface RequestEvent<
 /**
  * A `(event: RequestEvent) => Response` function exported from a `+server.js` file that corresponds to an HTTP verb (`GET`, `PUT`, `PATCH`, etc) and handles requests with that method.
  *
- * It receives `Params` as the first generic argument, which you can skip by using [generated types](/docs/types#generated-types) instead.
+ * It receives `Params` as the first generic argument, which you can skip by using [generated types](https://kit.svelte.dev/docs/types#generated-types) instead.
  */
 export interface RequestHandler<
 	Params extends Partial<Record<string, string>> = Partial<Record<string, string>>
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index 0e72dda4b..aebf0be71 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -100,12 +100,6 @@ export interface ClientHooks {
 	handleError: HandleClientError;
 }
 
-export interface ImportNode {
-	readonly name: string;
-	readonly dynamic: boolean;
-	readonly children: Generator<ImportNode>;
-}
-
 export class InternalServer extends Server {
 	init(options: ServerInitOptions): Promise<void>;
 	respond(
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3af108aa8..ba4fc6b0e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,12 +50,12 @@ importers:
       '@cloudflare/workers-types': ^3.14.0
       '@types/node': ^16.11.36
       '@types/ws': ^8.5.3
-      esbuild: ^0.14.48
+      esbuild: ^0.15.6
       typescript: ^4.8.2
       worktop: 0.8.0-next.14
     dependencies:
       '@cloudflare/workers-types': 3.14.0
-      esbuild: 0.14.54
+      esbuild: 0.15.6
       worktop: 0.8.0-next.14
     devDependencies:
       '@types/node': 16.11.42
@@ -68,12 +68,12 @@ importers:
       '@cloudflare/workers-types': ^3.14.0
       '@iarna/toml': ^2.2.5
       '@types/node': ^16.11.36
-      esbuild: ^0.14.48
+      esbuild: ^0.15.6
       typescript: ^4.8.2
     dependencies:
       '@cloudflare/workers-types': 3.14.0
       '@iarna/toml': 2.2.5
-      esbuild: 0.14.54
+      esbuild: 0.15.6
     devDependencies:
       '@cloudflare/kv-asset-handler': 0.2.0
       '@types/node': 16.11.42
@@ -89,18 +89,16 @@ importers:
       '@sveltejs/kit': workspace:*
       '@types/node': ^16.11.36
       '@types/set-cookie-parser': ^2.4.2
-      esbuild: ^0.14.48
+      esbuild: ^0.15.6
       rimraf: ^3.0.2
       rollup: ^2.78.1
       set-cookie-parser: ^2.4.8
-      tiny-glob: ^0.2.9
       typescript: ^4.8.2
       uvu: ^0.5.3
     dependencies:
       '@iarna/toml': 2.2.5
-      esbuild: 0.14.54
+      esbuild: 0.15.6
       set-cookie-parser: 2.5.0
-      tiny-glob: 0.2.9
     devDependencies:
       '@netlify/functions': 1.0.0
       '@rollup/plugin-commonjs': 22.0.1_rollup@2.78.1
@@ -120,7 +118,7 @@ importers:
       '@sveltejs/kit': workspace:*
       '@types/node': ^16.11.36
       c8: ^7.11.3
-      esbuild: ^0.14.48
+      esbuild: ^0.15.6
       node-fetch: ^3.2.4
       polka: ^1.0.0-next.22
       rimraf: ^3.0.2
@@ -129,7 +127,7 @@ importers:
       typescript: ^4.8.2
       uvu: ^0.5.3
     dependencies:
-      esbuild: 0.14.54
+      esbuild: 0.15.6
     devDependencies:
       '@rollup/plugin-json': 4.1.0_rollup@2.78.1
       '@sveltejs/kit': link:../kit
@@ -190,11 +188,11 @@ importers:
       '@sveltejs/kit': workspace:*
       '@types/node': ^16.11.36
       '@vercel/nft': ^0.22.0
-      esbuild: ^0.14.48
+      esbuild: ^0.15.6
       typescript: ^4.8.2
     dependencies:
       '@vercel/nft': 0.22.0
-      esbuild: 0.14.54
+      esbuild: 0.15.6
     devDependencies:
       '@sveltejs/kit': link:../kit
       '@types/node': 16.11.42
@@ -248,7 +246,7 @@ importers:
       svelte: ^3.48.0
       svelte-preprocess: ^4.10.6
       typescript: ^4.8.2
-      vite: 3.1.0-beta.2
+      vite: ^3.1.0
     dependencies:
       '@fontsource/fira-mono': 4.5.8
       '@lukeed/uuid': 2.0.0
@@ -259,7 +257,7 @@ importers:
       svelte: 3.48.0
       svelte-preprocess: 4.10.7_rzeu6t3zqu2xw2nsnbtx3gkaoe
       typescript: 4.8.2
-      vite: 3.1.0-beta.2
+      vite: 3.1.0
 
   packages/create-svelte/templates/skeleton:
     specifiers:
@@ -504,7 +502,7 @@ importers:
       svelte: ^3.44.0
       svelte-check: ^2.7.1
       typescript: ^4.8.2
-      vite: ^3.1.0-beta.1
+      vite: ^3.1.0
     devDependencies:
       '@sveltejs/kit': link:../../../..
       svelte: 3.48.0
@@ -518,7 +516,7 @@ importers:
       svelte: ^3.44.0
       svelte-check: ^2.7.1
       typescript: ^4.8.2
-      vite: ^3.1.0-beta.1
+      vite: ^3.1.0
     devDependencies:
       '@sveltejs/kit': link:../../../..
       svelte: 3.48.0
@@ -532,13 +530,13 @@ importers:
       svelte: ^3.44.0
       svelte-check: ^2.7.1
       typescript: ^4.8.2
-      vite: ^3.1.0-beta.1
+      vite: ^3.1.0
     devDependencies:
       '@sveltejs/kit': link:../../../..
       svelte: 3.48.0
       svelte-check: 2.8.0_svelte@3.48.0
       typescript: 4.8.2
-      vite: 3.1.0-beta.2
+      vite: 3.1.0
 
   packages/kit/test/build-errors/apps/server-only-module-dynamic-import:
     specifiers:
@@ -546,13 +544,13 @@ importers:
       svelte: ^3.44.0
       svelte-check: ^2.7.1
       typescript: ^4.8.2
-      vite: ^3.1.0-beta.1
+      vite: ^3.1.0
     devDependencies:
       '@sveltejs/kit': link:../../../..
       svelte: 3.48.0
       svelte-check: 2.8.0_svelte@3.48.0
       typescript: 4.8.2
-      vite: 3.1.0-beta.2
+      vite: 3.1.0
 
   packages/kit/test/prerendering/basics:
     specifiers:
@@ -948,15 +946,6 @@ packages:
     resolution: {integrity: sha512-F6lZuzu/vNl9AxOq1U3x2UCi68bcE1O59cKGQ5ntRh3lGQfd3vers6NUag02T7P+qFCHaAp78QN6fRmq12Y3iA==}
     dev: false
 
-  /@esbuild/linux-loong64/0.14.54:
-    resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==}
-    engines: {node: '>=12'}
-    cpu: [loong64]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /@esbuild/linux-loong64/0.15.6:
     resolution: {integrity: sha512-hqmVU2mUjH6J2ZivHphJ/Pdse2ZD+uGCHK0uvsiLDk/JnSedEVj77CiVUnbMKuU4tih1TZZL8tG9DExQg/GZsw==}
     engines: {node: '>=12'}
@@ -1969,15 +1958,6 @@ packages:
     resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
     dev: true
 
-  /esbuild-android-64/0.14.54:
-    resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [android]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-android-64/0.15.6:
     resolution: {integrity: sha512-Z1CHSgB1crVQi2LKSBwSkpaGtaloVz0ZIYcRMsvHc3uSXcR/x5/bv9wcZspvH/25lIGTaViosciS/NS09ERmVA==}
     engines: {node: '>=12'}
@@ -1986,15 +1966,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-android-arm64/0.14.54:
-    resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [android]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-android-arm64/0.15.6:
     resolution: {integrity: sha512-mvM+gqNxqKm2pCa3dnjdRzl7gIowuc4ga7P7c3yHzs58Im8v/Lfk1ixSgQ2USgIywT48QWaACRa3F4MG7djpSw==}
     engines: {node: '>=12'}
@@ -2003,15 +1974,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-darwin-64/0.14.54:
-    resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [darwin]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-darwin-64/0.15.6:
     resolution: {integrity: sha512-BsfVt3usScAfGlXJiGtGamwVEOTM8AiYiw1zqDWhGv6BncLXCnTg1As+90mxWewdTZKq3iIy8s9g8CKkrrAXVw==}
     engines: {node: '>=12'}
@@ -2020,15 +1982,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-darwin-arm64/0.14.54:
-    resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [darwin]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-darwin-arm64/0.15.6:
     resolution: {integrity: sha512-CnrAeJaEpPakUobhqO4wVSA4Zm6TPaI5UY4EsI62j9mTrjIyQPXA1n4Ju6Iu5TVZRnEqV6q8blodgYJ6CJuwCA==}
     engines: {node: '>=12'}
@@ -2037,15 +1990,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-freebsd-64/0.14.54:
-    resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [freebsd]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-freebsd-64/0.15.6:
     resolution: {integrity: sha512-+qFdmqi+jkAsxsNJkaWVrnxEUUI50nu6c3MBVarv3RCDCbz7ZS1a4ZrdkwEYFnKcVWu6UUE0Kkb1SQ1yGEG6sg==}
     engines: {node: '>=12'}
@@ -2054,15 +1998,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-freebsd-arm64/0.14.54:
-    resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [freebsd]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-freebsd-arm64/0.15.6:
     resolution: {integrity: sha512-KtQkQOhnNciXm2yrTYZMD3MOm2zBiiwFSU+dkwNbcfDumzzUprr1x70ClTdGuZwieBS1BM/k0KajRQX7r504Xw==}
     engines: {node: '>=12'}
@@ -2071,15 +2006,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-linux-32/0.14.54:
-    resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-linux-32/0.15.6:
     resolution: {integrity: sha512-IAkDNz3TpxwISTGVdQijwyHBZrbFgLlRi5YXcvaEHtgbmayLSDcJmH5nV1MFgo/x2QdKcHBkOYHdjhKxUAcPwg==}
     engines: {node: '>=12'}
@@ -2088,15 +2014,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-linux-64/0.14.54:
-    resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-linux-64/0.15.6:
     resolution: {integrity: sha512-gQPksyrEYfA4LJwyfTQWAZaVZCx4wpaLrSzo2+Xc9QLC+i/sMWmX31jBjrn4nLJCd79KvwCinto36QC7BEIU/A==}
     engines: {node: '>=12'}
@@ -2105,15 +2022,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-linux-arm/0.14.54:
-    resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==}
-    engines: {node: '>=12'}
-    cpu: [arm]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-linux-arm/0.15.6:
     resolution: {integrity: sha512-xZ0Bq2aivsthDjA/ytQZzxrxIZbG0ATJYMJxNeOIBc1zUjpbVpzBKgllOZMsTSXMHFHGrow6TnCcgwqY0+oEoQ==}
     engines: {node: '>=12'}
@@ -2122,15 +2030,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-linux-arm64/0.14.54:
-    resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-linux-arm64/0.15.6:
     resolution: {integrity: sha512-aovDkclFa6C9EdZVBuOXxqZx83fuoq8097xZKhEPSygwuy4Lxs8J4anHG7kojAsR+31lfUuxzOo2tHxv7EiNHA==}
     engines: {node: '>=12'}
@@ -2139,15 +2038,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-linux-mips64le/0.14.54:
-    resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==}
-    engines: {node: '>=12'}
-    cpu: [mips64el]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-linux-mips64le/0.15.6:
     resolution: {integrity: sha512-wVpW8wkWOGizsCqCwOR/G3SHwhaecpGy3fic9BF1r7vq4djLjUcA8KunDaBCjJ6TgLQFhJ98RjDuyEf8AGjAvw==}
     engines: {node: '>=12'}
@@ -2156,15 +2046,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-linux-ppc64le/0.14.54:
-    resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==}
-    engines: {node: '>=12'}
-    cpu: [ppc64]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-linux-ppc64le/0.15.6:
     resolution: {integrity: sha512-z6w6gsPH/Y77uchocluDC8tkCg9rfkcPTePzZKNr879bF4tu7j9t255wuNOCE396IYEGxY7y8u2HJ9i7kjCLVw==}
     engines: {node: '>=12'}
@@ -2173,15 +2054,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-linux-riscv64/0.14.54:
-    resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==}
-    engines: {node: '>=12'}
-    cpu: [riscv64]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-linux-riscv64/0.15.6:
     resolution: {integrity: sha512-pfK/3MJcmbfU399TnXW5RTPS1S+ID6ra+CVj9TFZ2s0q9Ja1F5A1VirUUvViPkjiw+Kq3zveyn6U09Wg1zJXrw==}
     engines: {node: '>=12'}
@@ -2190,15 +2062,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-linux-s390x/0.14.54:
-    resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==}
-    engines: {node: '>=12'}
-    cpu: [s390x]
-    os: [linux]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-linux-s390x/0.15.6:
     resolution: {integrity: sha512-OZeeDu32liefcwAE63FhVqM4heWTC8E3MglOC7SK0KYocDdY/6jyApw0UDkDHlcEK9mW6alX/SH9r3PDjcCo/Q==}
     engines: {node: '>=12'}
@@ -2207,15 +2070,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-netbsd-64/0.14.54:
-    resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [netbsd]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-netbsd-64/0.15.6:
     resolution: {integrity: sha512-kaxw61wcHMyiEsSsi5ut1YYs/hvTC2QkxJwyRvC2Cnsz3lfMLEu8zAjpBKWh9aU/N0O/gsRap4wTur5GRuSvBA==}
     engines: {node: '>=12'}
@@ -2224,15 +2078,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-openbsd-64/0.14.54:
-    resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [openbsd]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-openbsd-64/0.15.6:
     resolution: {integrity: sha512-CuoY60alzYfIZapUHqFXqXbj88bbRJu8Fp9okCSHRX2zWIcGz4BXAHXiG7dlCye5nFVrY72psesLuWdusyf2qw==}
     engines: {node: '>=12'}
@@ -2241,15 +2086,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-sunos-64/0.14.54:
-    resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [sunos]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-sunos-64/0.15.6:
     resolution: {integrity: sha512-1ceefLdPWcd1nW/ZLruPEYxeUEAVX0YHbG7w+BB4aYgfknaLGotI/ZvPWUZpzhC8l1EybrVlz++lm3E6ODIJOg==}
     engines: {node: '>=12'}
@@ -2258,15 +2094,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-windows-32/0.14.54:
-    resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==}
-    engines: {node: '>=12'}
-    cpu: [ia32]
-    os: [win32]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-windows-32/0.15.6:
     resolution: {integrity: sha512-pBqdOsKqCD5LRYiwF29PJRDJZi7/Wgkz46u3d17MRFmrLFcAZDke3nbdDa1c8YgY78RiemudfCeAemN8EBlIpA==}
     engines: {node: '>=12'}
@@ -2275,15 +2102,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-windows-64/0.14.54:
-    resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==}
-    engines: {node: '>=12'}
-    cpu: [x64]
-    os: [win32]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-windows-64/0.15.6:
     resolution: {integrity: sha512-KpPOh4aTOo//g9Pk2oVAzXMpc9Sz9n5A9sZTmWqDSXCiiachfFhbuFlsKBGATYCVitXfmBIJ4nNYYWSOdz4hQg==}
     engines: {node: '>=12'}
@@ -2292,15 +2110,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild-windows-arm64/0.14.54:
-    resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==}
-    engines: {node: '>=12'}
-    cpu: [arm64]
-    os: [win32]
-    requiresBuild: true
-    dev: false
-    optional: true
-
   /esbuild-windows-arm64/0.15.6:
     resolution: {integrity: sha512-DB3G2x9OvFEa00jV+OkDBYpufq5x/K7a6VW6E2iM896DG4ZnAvJKQksOsCPiM1DUaa+DrijXAQ/ZOcKAqf/3Hg==}
     engines: {node: '>=12'}
@@ -2309,35 +2118,6 @@ packages:
     requiresBuild: true
     optional: true
 
-  /esbuild/0.14.54:
-    resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==}
-    engines: {node: '>=12'}
-    hasBin: true
-    requiresBuild: true
-    optionalDependencies:
-      '@esbuild/linux-loong64': 0.14.54
-      esbuild-android-64: 0.14.54
-      esbuild-android-arm64: 0.14.54
-      esbuild-darwin-64: 0.14.54
-      esbuild-darwin-arm64: 0.14.54
-      esbuild-freebsd-64: 0.14.54
-      esbuild-freebsd-arm64: 0.14.54
-      esbuild-linux-32: 0.14.54
-      esbuild-linux-64: 0.14.54
-      esbuild-linux-arm: 0.14.54
-      esbuild-linux-arm64: 0.14.54
-      esbuild-linux-mips64le: 0.14.54
-      esbuild-linux-ppc64le: 0.14.54
-      esbuild-linux-riscv64: 0.14.54
-      esbuild-linux-s390x: 0.14.54
-      esbuild-netbsd-64: 0.14.54
-      esbuild-openbsd-64: 0.14.54
-      esbuild-sunos-64: 0.14.54
-      esbuild-windows-32: 0.14.54
-      esbuild-windows-64: 0.14.54
-      esbuild-windows-arm64: 0.14.54
-    dev: false
-
   /esbuild/0.15.6:
     resolution: {integrity: sha512-sgLOv3l4xklvXzzczhRwKRotyrfyZ2i1fCS6PTOLPd9wevDPArGU8HFtHrHCOcsMwTjLjzGm15gvC8uxVzQf+w==}
     engines: {node: '>=12'}
@@ -4864,33 +4644,6 @@ packages:
     optionalDependencies:
       fsevents: 2.3.2
 
-  /vite/3.1.0-beta.2:
-    resolution: {integrity: sha512-JD1I8S29RFAEL94/bamrdJd3VlLQROcGwQxb8X1hRWDtwmPTves8TmZCaQ4g460j6zZxyLYwSVHwkGCQ2k07og==}
-    engines: {node: ^14.18.0 || >=16.0.0}
-    hasBin: true
-    peerDependencies:
-      less: '*'
-      sass: '*'
-      stylus: '*'
-      terser: ^5.4.0
-    peerDependenciesMeta:
-      less:
-        optional: true
-      sass:
-        optional: true
-      stylus:
-        optional: true
-      terser:
-        optional: true
-    dependencies:
-      esbuild: 0.15.6
-      postcss: 8.4.16
-      resolve: 1.22.1
-      rollup: 2.78.1
-    optionalDependencies:
-      fsevents: 2.3.2
-    dev: true
-
   /vscode-oniguruma/1.6.2:
     resolution: {integrity: sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==}
     dev: true


Test patch:
diff --git a/packages/kit/src/utils/unit_test.js b/packages/kit/src/utils/unit_test.js
new file mode 100644
index 000000000..eca30f1b4
--- /dev/null
+++ b/packages/kit/src/utils/unit_test.js
@@ -0,0 +1,11 @@
+import { suite } from 'uvu';
+
+/**
+ * @param {string} name
+ * @param {(suite: import('uvu').Test<import('uvu').Context>) => void} fn
+ */
+export function describe(name, fn) {
+	const s = suite(name);
+	fn(s);
+	s.run();
+}
diff --git a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js
index cb69d5781..7a97721c2 100644
--- a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js
+++ b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js
@@ -7,10 +7,19 @@ export function load() {
 
 /** @type {import('./$types').Actions} */
 export const actions = {
-	default: async ({ request }) => {
+	login: async ({ request }) => {
 		const fields = await request.formData();
 		return {
 			result: fields.get('username')
 		};
+	},
+	register: async ({ request }) => {
+		const fields = await request.formData();
+		return {
+			result: 'register: ' + fields.get('username')
+		};
+	},
+	slow: async () => {
+		await new Promise((resolve) => setTimeout(resolve, 500));
 	}
 };
diff --git a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
index db5e38b63..748b0986e 100644
--- a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
@@ -3,11 +3,29 @@
 
 	/** @type {import('./$types').ActionData} */
 	export let form;
+
+	/** @type {AbortController | undefined} */
+	let previous;
+	let count = 0;
 </script>
 
 <pre>{JSON.stringify(form)}</pre>
 
-<form method="post" use:enhance>
+<form method="post" action="?/login" use:enhance>
 	<input name="username" type="text" />
-	<button>Submit</button>
+	<button class="form1">Submit</button>
+	<button class="form1-register" formAction="?/register">Submit</button>
+</form>
+
+<span class="count">{count}</span>
+<form
+	method="post"
+	action="?/slow"
+	use:enhance={({ controller }) => {
+		previous?.abort();
+		previous = controller;
+		return () => count++;
+	}}
+>
+	<button class="form2">Submit</button>
 </form>
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index e9161307f..16c81f3d7 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1792,9 +1792,40 @@ test.describe('Actions', () => {
 		await page.type('input[name="username"]', 'foo');
 		await Promise.all([
 			page.waitForRequest((request) => request.url().includes('/actions/enhance')),
-			page.click('button')
+			page.click('button.form1')
 		]);
 
 		await expect(page.locator('pre')).toHaveText(JSON.stringify({ result: 'foo' }));
 	});
+
+	test('use:enhance abort controller', async ({ page, javaScriptEnabled }) => {
+		await page.goto('/actions/enhance');
+
+		expect(await page.textContent('span.count')).toBe('0');
+
+		if (javaScriptEnabled) {
+			await Promise.all([
+				page.waitForRequest((request) => request.url().includes('/actions/enhance')),
+				page.click('button.form2'),
+				page.click('button.form2')
+			]);
+			await page.waitForTimeout(500); // to make sure locator doesn't run exactly between submission 1 and 2
+
+			await expect(page.locator('span.count')).toHaveText('1');
+		}
+	});
+
+	test('use:enhance button with formAction', async ({ page, app }) => {
+		await page.goto('/actions/enhance');
+
+		expect(await page.textContent('pre')).toBe(JSON.stringify(null));
+
+		await page.type('input[name="username"]', 'foo');
+		await Promise.all([
+			page.waitForRequest((request) => request.url().includes('/actions/enhance')),
+			page.click('button.form1-register')
+		]);
+
+		await expect(page.locator('pre')).toHaveText(JSON.stringify({ result: 'register: foo' }));
+	});
 });
diff --git a/packages/kit/test/apps/dev-only/test/test.js b/packages/kit/test/apps/dev-only/test/test.js
index 2fadb64ea..7010d7a51 100644
--- a/packages/kit/test/apps/dev-only/test/test.js
+++ b/packages/kit/test/apps/dev-only/test/test.js
@@ -9,7 +9,7 @@ test.describe('$env', () => {
 	test('$env/dynamic/private is not statically importable from the client', async ({ request }) => {
 		const resp = await request.get('/env/dynamic-private');
 		expect(await resp.text()).toMatch(
-			/.*Cannot import \$env\/dynamic\/private into client-side code:.*/gs
+			/.*Cannot import \$env\/dynamic\/private into public-facing code:.*/gs
 		);
 	});
 
@@ -18,21 +18,21 @@ test.describe('$env', () => {
 	}) => {
 		const resp = await request.get('/env/dynamic-private-dynamic-import');
 		expect(await resp.text()).toMatch(
-			/.*Cannot import \$env\/dynamic\/private into client-side code:.*/gs
+			/.*Cannot import \$env\/dynamic\/private into public-facing code:.*/gs
 		);
 	});
 
 	test('$env/static/private is not statically importable from the client', async ({ request }) => {
 		const resp = await request.get('/env/static-private');
 		expect(await resp.text()).toMatch(
-			/.*Cannot import \$env\/static\/private into client-side code:.*/gs
+			/.*Cannot import \$env\/static\/private into public-facing code:.*/gs
 		);
 	});
 
 	test('$env/static/private is not dynamically importable from the client', async ({ request }) => {
 		const resp = await request.get('/env/static-private-dynamic-import');
 		expect(await resp.text()).toMatch(
-			/.*Cannot import \$env\/static\/private into client-side code:.*/gs
+			/.*Cannot import \$env\/static\/private into public-facing code:.*/gs
 		);
 	});
 });
@@ -41,13 +41,13 @@ test.describe('server-only modules', () => {
 	test('server-only module is not statically importable from the client', async ({ request }) => {
 		const resp = await request.get('/server-only-modules/static-import');
 		expect(await resp.text()).toMatch(
-			/.*Cannot import \$lib\/test.server.js into client-side code:.*/gs
+			/.*Cannot import \$lib\/test.server.js into public-facing code:.*/gs
 		);
 	});
 	test('server-only module is not dynamically importable from the client', async ({ request }) => {
 		const resp = await request.get('/server-only-modules/dynamic-import');
 		expect(await resp.text()).toMatch(
-			/.*Cannot import \$lib\/test.server.js into client-side code:.*/gs
+			/.*Cannot import \$lib\/test.server.js into public-facing code:.*/gs
 		);
 	});
 });
@@ -56,13 +56,13 @@ test.describe('server-only folder', () => {
 	test('server-only folder is not statically importable from the client', async ({ request }) => {
 		const resp = await request.get('/server-only-folder/static-import');
 		expect(await resp.text()).toMatch(
-			/.*Cannot import \$lib\/server\/blah\/test.js into client-side code:.*/gs
+			/.*Cannot import \$lib\/server\/blah\/test.js into public-facing code:.*/gs
 		);
 	});
 	test('server-only folder is not dynamically importable from the client', async ({ request }) => {
 		const resp = await request.get('/server-only-folder/dynamic-import');
 		expect(await resp.text()).toMatch(
-			/.*Cannot import \$lib\/server\/blah\/test.js into client-side code:.*/gs
+			/.*Cannot import \$lib\/server\/blah\/test.js into public-facing code:.*/gs
 		);
 	});
 });
diff --git a/packages/kit/test/build-errors/apps/server-only-folder-dynamic-import/package.json b/packages/kit/test/build-errors/apps/server-only-folder-dynamic-import/package.json
index e407678f5..9f80575ef 100644
--- a/packages/kit/test/build-errors/apps/server-only-folder-dynamic-import/package.json
+++ b/packages/kit/test/build-errors/apps/server-only-folder-dynamic-import/package.json
@@ -14,7 +14,7 @@
 		"svelte": "^3.44.0",
 		"svelte-check": "^2.7.1",
 		"typescript": "^4.8.2",
-		"vite": "^3.1.0-beta.1"
+		"vite": "^3.1.0"
 	},
 	"type": "module"
 }
diff --git a/packages/kit/test/build-errors/apps/server-only-folder/package.json b/packages/kit/test/build-errors/apps/server-only-folder/package.json
index e7bf50ae9..b547e6422 100644
--- a/packages/kit/test/build-errors/apps/server-only-folder/package.json
+++ b/packages/kit/test/build-errors/apps/server-only-folder/package.json
@@ -14,7 +14,7 @@
 		"svelte": "^3.44.0",
 		"svelte-check": "^2.7.1",
 		"typescript": "^4.8.2",
-		"vite": "^3.1.0-beta.1"
+		"vite": "^3.1.0"
 	},
 	"type": "module"
 }
diff --git a/packages/kit/test/build-errors/apps/server-only-module-dynamic-import/package.json b/packages/kit/test/build-errors/apps/server-only-module-dynamic-import/package.json
index 0cdc65ca6..2755a61c8 100644
--- a/packages/kit/test/build-errors/apps/server-only-module-dynamic-import/package.json
+++ b/packages/kit/test/build-errors/apps/server-only-module-dynamic-import/package.json
@@ -14,7 +14,7 @@
 		"svelte": "^3.44.0",
 		"svelte-check": "^2.7.1",
 		"typescript": "^4.8.2",
-		"vite": "^3.1.0-beta.1"
+		"vite": "^3.1.0"
 	},
 	"type": "module"
 }
diff --git a/packages/kit/test/build-errors/apps/server-only-module/package.json b/packages/kit/test/build-errors/apps/server-only-module/package.json
index 819943cde..b03d4e027 100644
--- a/packages/kit/test/build-errors/apps/server-only-module/package.json
+++ b/packages/kit/test/build-errors/apps/server-only-module/package.json
@@ -14,7 +14,7 @@
 		"svelte": "^3.44.0",
 		"svelte-check": "^2.7.1",
 		"typescript": "^4.8.2",
-		"vite": "^3.1.0-beta.1"
+		"vite": "^3.1.0"
 	},
 	"type": "module"
 }
diff --git a/packages/kit/test/build-errors/env.spec.js b/packages/kit/test/build-errors/env.spec.js
index c30a19bfc..1dba0a958 100644
--- a/packages/kit/test/build-errors/env.spec.js
+++ b/packages/kit/test/build-errors/env.spec.js
@@ -11,7 +11,7 @@ test('$env/dynamic/private is not statically importable from the client', () =>
 				stdio: 'pipe',
 				timeout: 15000
 			}),
-		/.*Cannot import \$env\/dynamic\/private into client-side code:.*/gs
+		/.*Cannot import \$env\/dynamic\/private into public-facing code:.*/gs
 	);
 });
 
@@ -23,7 +23,7 @@ test('$env/dynamic/private is not dynamically importable from the client', () =>
 				stdio: 'pipe',
 				timeout: 15000
 			}),
-		/.*Cannot import \$env\/dynamic\/private into client-side code:.*/gs
+		/.*Cannot import \$env\/dynamic\/private into public-facing code:.*/gs
 	);
 });
 
@@ -35,7 +35,7 @@ test('$env/static/private is not statically importable from the client', () => {
 				stdio: 'pipe',
 				timeout: 15000
 			}),
-		/.*Cannot import \$env\/static\/private into client-side code:.*/gs
+		/.*Cannot import \$env\/static\/private into public-facing code:.*/gs
 	);
 });
 
@@ -47,7 +47,7 @@ test('$env/static/private is not dynamically importable from the client', () =>
 				stdio: 'pipe',
 				timeout: 15000
 			}),
-		/.*Cannot import \$env\/static\/private into client-side code:.*/gs
+		/.*Cannot import \$env\/static\/private into public-facing code:.*/gs
 	);
 });
 
diff --git a/packages/kit/test/build-errors/server-only.spec.js b/packages/kit/test/build-errors/server-only.spec.js
index 7e620820e..f11b33beb 100644
--- a/packages/kit/test/build-errors/server-only.spec.js
+++ b/packages/kit/test/build-errors/server-only.spec.js
@@ -11,7 +11,7 @@ test('$lib/*.server.* is not statically importable from the client', () => {
 				stdio: 'pipe',
 				timeout: 15000
 			}),
-		/.*Cannot import \$lib\/test.server.js into client-side code:.*/gs
+		/.*Cannot import \$lib\/test.server.js into public-facing code:.*/gs
 	);
 });
 
@@ -23,7 +23,7 @@ test('$lib/*.server.* is not dynamically importable from the client', () => {
 				stdio: 'pipe',
 				timeout: 15000
 			}),
-		/.*Cannot import \$lib\/test.server.js into client-side code:.*/gs
+		/.*Cannot import \$lib\/test.server.js into public-facing code:.*/gs
 	);
 });
 
@@ -35,7 +35,7 @@ test('$lib/server/* is not statically importable from the client', () => {
 				stdio: 'pipe',
 				timeout: 15000
 			}),
-		/.*Cannot import \$lib\/server\/something\/test.js into client-side code:.*/gs
+		/.*Cannot import \$lib\/server\/something\/test.js into public-facing code:.*/gs
 	);
 });
 
@@ -47,7 +47,7 @@ test('$lib/server/* is not dynamically importable from the client', () => {
 				stdio: 'pipe',
 				timeout: 15000
 			}),
-		/.*Cannot import \$lib\/server\/something\/test.js into client-side code:.*/gs
+		/.*Cannot import \$lib\/server\/something\/test.js into public-facing code:.*/gs
 	);
 });
 
diff --git a/packages/kit/test/prerendering/paths-base/src/routes/assets/+page.svelte b/packages/kit/test/prerendering/paths-base/src/routes/assets/+page.svelte
new file mode 100644
index 000000000..2d15557a2
--- /dev/null
+++ b/packages/kit/test/prerendering/paths-base/src/routes/assets/+page.svelte
@@ -0,0 +1,5 @@
+<script>
+	import logo from './logo.svg';
+</script>
+
+<img src={logo} alt="Svelte logo" />
diff --git a/packages/kit/test/prerendering/paths-base/src/routes/assets/logo.svg b/packages/kit/test/prerendering/paths-base/src/routes/assets/logo.svg
new file mode 100644
index 000000000..49492a83c
--- /dev/null
+++ b/packages/kit/test/prerendering/paths-base/src/routes/assets/logo.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157" style="fill:#ff3e00"/><path d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287" style="fill:#fff"/></svg>
\ No newline at end of file
diff --git a/packages/kit/test/prerendering/paths-base/test/test.js b/packages/kit/test/prerendering/paths-base/test/test.js
index d2a47907a..36d7b79c0 100644
--- a/packages/kit/test/prerendering/paths-base/test/test.js
+++ b/packages/kit/test/prerendering/paths-base/test/test.js
@@ -18,4 +18,9 @@ test('prerenders /path-base/dynamic/foo', () => {
 	assert.ok(content.includes('<h1>foo</h1>'));
 });
 
+test('prerenders /path-base/assets', () => {
+	const content = read('assets.html');
+	assert.match(content, /<img[^>]+src="\/path-base\//u);
+});
+
 test.run();