Skip to content

chore: replace ssrLoadModule with the RunnableDevEnvironment API - #16490

Merged
Rich-Harris merged 8 commits into
version-3from
runnable-dev-environment
Jul 24, 2026
Merged

chore: replace ssrLoadModule with the RunnableDevEnvironment API#16490
Rich-Harris merged 8 commits into
version-3from
runnable-dev-environment

Conversation

@teemingc

@teemingc teemingc commented Jul 23, 2026

Copy link
Copy Markdown
Member

closes #11932

Makes sense to split this off from #16464 since it's a relatively simple change. Technically, this means we're 100% on the environment API, but everything still runs on Node.js only. Main benefit is that we have separate module graphs between the client and server now and one step closer to adopting other parts of the environment API (fetchable dev environments)


Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

@pkg-svelte-dev

pkg-svelte-dev Bot commented Jul 23, 2026

Copy link
Copy Markdown

Install the latest version of @sveltejs/kit from 77883dc:

pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/77883dc418786b14d3c0f431a42fc8a2b5cb7f7d

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/kit/pr/16490

@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 28a0e73

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.

This PR includes no changesets

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

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Comment on lines -339 to -344
try {
vite.ssrFixStacktrace(error);
} catch {
// ssrFixStacktrace can fail on StackBlitz web containers and we don't know why
// by ignoring it the line numbers are wrong, but at least we can show the error
}

@teemingc teemingc Jul 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got rid of an indent, hence it looks like all the stacktrace fixing has changed.

AFAIK Vite already applies this stack trace fix when you use the environment API instead of ssrLoadModule

remove_static_middlewares(vite.middlewares);

vite.middlewares.use(async (req, res) => {
// Vite throws a Cannot read properties of undefined (reading 'wrapDynamicImport')

@teemingc teemingc Jul 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens whenever someone runs Vitest with browser testing (you can reproduce by running pnpm test:unit in the basic test app)

@svelte-docs-bot

Copy link
Copy Markdown

@teemingc
teemingc requested a review from Rich-Harris July 23, 2026 17:18

@vercel vercel Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

The change file watcher dereferences manifest_data.nodes before manifest_data is assigned, crashing with a TypeError if a route file is edited before the dev server serves its first HTTP request.

Fix on Vercel

…ore `manifest_data` is assigned, crashing with a TypeError if a route file is edited before the dev server serves its first HTTP request.

This commit fixes the issue reported at packages/kit/src/exports/vite/dev/index.js:416

## Bug

In `packages/kit/src/exports/vite/dev/index.js`, commit `90ecc99` ("replace ssrLoadModule") removed the eager `await update_manifest()` that previously ran during `dev()` setup, and instead runs it lazily on the first HTTP request:

```js
vite.middlewares.use(async (req, res) => {
    if (!manifest_created) {
        await update_manifest();   // this assigns `manifest_data`
        manifest_created = true;
    }
    ...
```

`update_manifest()` is the only thing that assigns the module-scoped `let manifest_data;` (via `({ manifest_data } = sync.create(...))`).

However, the file watchers are registered during setup — *before* the middleware installer is returned and therefore before any request is served. The `change` watcher dereferences `manifest_data`:

```js
watch('change', (file) => {
    if (timeout || !/+(page|layout|server).*$/.test(file)) return;
    sync.update(svelte_config, manifest_data, file, root);
});
```

and `sync.update` (`packages/kit/src/core/sync/sync.js`) immediately iterates it:

```js
export function update(config, manifest_data, file, root) {
    const node_analyser = create_node_analyser(root);
    for (const node of manifest_data.nodes) { ... }   // TypeError if undefined
    ...
}
```

### Concrete trigger

1.  Start the dev server.
2.  Before making any HTTP request (e.g. before opening the browser), edit a `+page.svelte`, `+layout.svelte`, or `+server.js` file.
3.  The `change` watcher fires, `timeout` is `null`, the regex matches, so `sync.update(svelte_config, undefined, file, root)` is called.
4.  `sync.update` throws `TypeError: Cannot read properties of undefined (reading 'nodes')`.

Prior to this PR the eager `await update_manifest()` guaranteed `manifest_data` was assigned before any watcher could fire, so this window did not exist.

## Fix

Guard the `change` handler with an early return when `manifest_data` is still undefined. In that case there is nothing to incrementally update — the manifest will be created from scratch (`sync.create`) on the first request via the deferred `update_manifest()` call. This preserves the intended deferral of the `runner.import`-dependent work (which is why the eager call was removed) while avoiding the crash.

```js
watch('change', (file) => {
    if (!manifest_data) return;
    if (timeout || !/+(page|layout|server).*$/.test(file)) return;
    sync.update(svelte_config, manifest_data, file, root);
});
```

Note the `add`/`unlink` watchers call `update_manifest` (which assigns `manifest_data` itself and wraps the `runner.import` work in a try/catch), so they are not affected by this window.


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: teemingc <chewteeming01@gmail.com>
vite.middlewares.use(async (req, res) => {
// Vite throws a Cannot read properties of undefined (reading 'wrapDynamicImport')
// if you try to run ssr.runner.import before the server has started so
// we do it inside here to avoid that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this error actually comes from @vitest/mocker's dynamic import wrapper rather than Vite itself (load_explicit_env imports fine on a server that never listens). If so, the add/unlink watchers can still crash a Vitest watch run since they call update_manifest directly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm maybe it'll be better to move the load_and_validate out of the update_manifest so that we can do it earlier as it originally was

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, but make sure that validation still reruns on route changes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that validation only landed in #16189, so the "original" update_manifest predates it. matchers() might be the tidy spot: fresh routes per request, and it would also catch params file edits, which currently do not retrigger validation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hang on, update_manifest is synchronous, it returns void. so this will re-run on every request

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errr wait. i might be looking at old code

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay I was looking at #16464 — will fix it there

if (module) {
server.moduleGraph.invalidateModule(module);
}
invalidate_module(server, id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to only invalidate before the full reload, now it reloads the module in every environment too. Was that deliberate or just a side effect of sharing the helper?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I knew about it but didn't think there would be much of an impact. The full reload takes place anyway

Comment thread packages/kit/src/exports/vite/dev/index.js Outdated
Comment thread packages/kit/src/exports/vite/dev/index.js Outdated
@Nic-Polumeyv

Copy link
Copy Markdown
Contributor

Under Vitest browser mode nothing reaches kit's middleware, so manifest_data never gets set and $app/manifest ends up empty. The comment in create_manifest_data_module still describes the old behaviour.

teemingc and others added 2 commits July 24, 2026 02:01
Co-authored-by: Nic Polumeyv <nicolas.polum@outlook.com>
Co-authored-by: Nic Polumeyv <nicolas.polum@outlook.com>

@Rich-Harris Rich-Harris left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not totally clear on the init_manifest conversation, but it feels like something we can iterate on if necessary

Comment thread packages/kit/src/exports/vite/index.js Outdated
@Rich-Harris

Copy link
Copy Markdown
Member

gonna go ahead and merge this (once green) independently of the rest of the stack, in the name of reducing merge conflict risk with other PRs

Comment thread packages/kit/src/exports/vite/dev/index.js Outdated
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
@Rich-Harris
Rich-Harris merged commit 2c08533 into version-3 Jul 24, 2026
21 checks passed
@Rich-Harris
Rich-Harris deleted the runnable-dev-environment branch July 24, 2026 14:14
@teemingc teemingc linked an issue Jul 31, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants