Skip to content

chore: write env modules to disk - #16807

Merged
Rich-Harris merged 30 commits into
version-3from
generated-env-modules
Aug 15, 2026
Merged

chore: write env modules to disk#16807
Rich-Harris merged 30 commits into
version-3from
generated-env-modules

Conversation

@Rich-Harris

Copy link
Copy Markdown
Member

Ref #15212.

Something we've wanted to do for a while is get rid of the virtual modules, in favour of writing stuff to disk. The system becomes a lot easier to understand when it involves real artifacts instead of the crazy indirection we have going on at the moment.

I started with the $app/env/* stuff because that's likely to be the most challenging, since it involves some mad science around starting up a mini Vite dev server to load the src/env.ts module so that we can analyse it so that we can create a generated module that also loads the src/env.ts module... anyway, it works, and so I assume we will be able to do the same for the other __sveltekit/* modules.

(We could probably replace the resolveId logic with an alias that just points to the generated folder, same as we have for $app/* currently. That can wait for a follow-up PR though.)

Another thing I'm doing in this PR is creating the plugin in a separate module, rather than adding to the chaos in vite/index.js. It involves a little bit of duplication, but it makes everything so much more self-contained, and makes the coupling between different plugins more explicit (e.g. the callback).


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 Aug 14, 2026

Copy link
Copy Markdown

Install the latest version of @sveltejs/kit from ea22879:

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

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

@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ea22879

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Patch

Not sure what this means? Click here to learn what changesets are.

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

@svelte-docs-bot

Copy link
Copy Markdown

Comment thread packages/kit/src/exports/vite/plugins/env-vars.js
Comment thread packages/kit/src/exports/vite/plugins/env-vars.js Outdated
vercel Bot and others added 2 commits August 14, 2026 20:12
…osixified `out_dir` (backslashes), so index.js's posix-keyed `chunk.modules` lookup for the dynamic public env module never matches, forcing `uses_env_dynamic_public` to always be false.

This commit fixes the issue reported at packages/kit/src/exports/vite/plugins/env-vars.js:139

## Bug

`packages/kit/src/exports/vite/plugins/env-vars.js` computed:

```js
out_dir = path.resolve(c.root, out); // native separators
```

and its `resolveId` handler returns module ids derived from that value, e.g.:

```js
const dir = ` 
```

On **Windows**, `path.resolve` produces backslash-separated paths (`C:\proj.svelte-kit`), so the base of the returned module id contains backslashes: `C:\proj.svelte-kit/generated/env/public/client.js`. Rollup/rolldown store the module id exactly as returned by `resolveId`, so `chunk.modules` is keyed with that mixed-separator string.

Meanwhile `packages/kit/src/exports/vite/index.js` (~line 1768) looks the module up using a fully posix key:

```js
out_dir = posixify(kit.outDir); // line 386, forward slashes
// ...
const uses_env_dynamic_public =
  has_explicit_dynamic_public_env &&
  client_chunks.some(
    (chunk) =>
      chunk.type === 'chunk' &&
      chunk.modules[` 
```

Because the two keys differ in the base separators on Windows (`` vs `/`), the lookup never matches. **Trigger:** a Windows build with `bundleStrategy: 'split'` and a project that imports `$app/env/public` / uses a dynamic (non-static) public env var. `uses_env_dynamic_public` is forced to `false`, so the code that arranges correct preloading/loading of dynamic public env for the split bundle is skipped, breaking runtime public env vars on Windows.

The rest of the codebase consistently uses `posixify(kit.outDir)` for `out_dir` precisely so module ids match — this plugin was the sole divergence.

## Fix

Posixify `out_dir` in the plugin so the generated module ids use forward slashes and match index.js's lookup key:

```js
import { posixify } from '../../../utils/os.js';
// ...
out_dir = posixify(path.resolve(c.root, out));
```

`posixify` (from `packages/kit/src/utils/os.js`) simply replaces `\` with `/`, which is a no-op on POSIX systems and corrects the separators on Windows, restoring the key match.


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Rich-Harris <hello@rich-harris.dev>
…unning is no longer handled: new env files are ignored until restart, and deleting an existing env file throws because `resolved_entry` is never re-resolved.

This commit fixes the issue reported at packages/kit/src/exports/vite/plugins/env-vars.js:149

## Bug

The `plugin_env_vars` plugin in `packages/kit/src/exports/vite/plugins/env-vars.js` computes `resolved_entry` only once, in `buildStart`, and only regenerates env modules via `handleHotUpdate` when the changed file is already in `deps`:

```js
async buildStart() {
  resolved_entry = resolve_entry(path.join(resolved_config.root, entry)) ?? null;
  await generate();
},
async handleHotUpdate(update) {
  if (!deps.has(update.file)) return;
  await generate();
},
```

This replaced a watcher previously in `plugin_virtual_modules.configureServer` (in `index.js`) that listened to `server.watcher.on('all', ...)`, re-resolved the entry via `resolve_explicit_env_entry`, regenerated, and triggered a full reload on any add/remove/change of the env entry. (`resolve_explicit_env_entry` is now only imported in `index.js`, never called.)

Vite's `handleHotUpdate` only fires for `change` events on files it already tracks — it is **not** called for file `add`/`unlink`. That produces two concrete dev regressions:

1.  **Creating `src/env.ts` after start**: If it didn't exist at startup, `resolved_entry` is `null` and `deps` is empty (`load_explicit_env` returns an empty dep set when `file` is null). The `add` event doesn't reach `handleHotUpdate`, and even if it did the file isn't in `deps`, so the new env vars are never generated until a manual restart.
    
2.  **Deleting an existing `src/env.ts`**: The entry file is in `deps` (the dependency-scanner `load` hook records it). On delete, `handleHotUpdate` fires with the deleted file in `deps`, calling `generate()` → `sync.env(config, resolved_entry, ...)` with the stale, now-nonexistent `resolved_entry`, so `load_explicit_env` does `runner.import(file)` on a missing module and throws (`ERR_MODULE_NOT_FOUND`). `resolved_entry` is never reset to `null`.
    

The dev watcher in `dev/index.js` handles app/error templates, service worker and hooks, but not `src/env`, so it doesn't compensate.

## Fix

Added a `configureServer` hook to the env-vars plugin that listens for `add`/`unlink` events, re-resolves the entry, updates `resolved_entry`, regenerates the modules, and sends a `full-reload` — mirroring the removed watcher. `change` events remain handled by `handleHotUpdate` (for the entry and its transitive deps), so there is no double processing.

```js
configureServer(server) {
  const on_entry_add_unlink = async (file) => {
    const resolved = resolve_entry(path.join(resolved_config.root, entry)) ?? null;
    if (file === resolved_entry || file === resolved) {
      resolved_entry = resolved;
      await generate();
      server.hot.send({ type: 'full-reload' });
    }
  };
  server.watcher.on('add', on_entry_add_unlink);
  server.watcher.on('unlink', on_entry_add_unlink);
},
```

This restores dev handling for both creating (case 1) and deleting (case 2, `resolved` becomes `null` so `generate()` runs with a null entry and produces empty env modules instead of throwing) the env entry. The `server.watcher`/`server.hot` usage matches existing patterns in `dev/index.js`.


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Rich-Harris <hello@rich-harris.dev>
Comment thread packages/kit/src/core/env.js Outdated
Comment thread packages/kit/src/exports/vite/plugins/env-vars.js
Comment thread packages/kit/src/exports/vite/plugins/env-vars.js Outdated
Comment thread packages/kit/src/exports/vite/plugins/env-vars.js
Comment thread packages/kit/src/exports/vite/utils.js Outdated
Comment thread packages/kit/src/exports/vite/plugins/env-vars.js Outdated
Comment thread packages/kit/src/exports/vite/plugins/env-vars.js Outdated
Co-authored-by: Nic Polumeyv <nicolas.polum@outlook.com>
Comment thread packages/kit/src/exports/vite/index.js Outdated
Rich-Harris and others added 2 commits August 14, 2026 17:19
Co-authored-by: Nic Polumeyv <nicolas.polum@outlook.com>
Comment thread packages/kit/src/exports/vite/plugins/env-vars.js

@Nic-Polumeyv Nic-Polumeyv 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.

What a roller coaster. LGTM!

@Rich-Harris

Copy link
Copy Markdown
Member Author

feels good! appreciate the quarterbacking on this, some of the Vite nuances can be pretty subtle

@Rich-Harris
Rich-Harris merged commit 1b4fa21 into version-3 Aug 15, 2026
27 checks passed
@Rich-Harris
Rich-Harris deleted the generated-env-modules branch August 15, 2026 00:35
@teemingc teemingc linked an issue Aug 16, 2026 that may be closed by this pull request
Rich-Harris added a commit that referenced this pull request Aug 17, 2026
Follow-up to #16807. Instead of using a `resolveId` hook, we can use an
alias for all the generated modules — every module ID like
`<sveltekit:generated>/foo.js` corresponds to
`.svelte-kit/generated/(build|dev)/foo.js`, making things a little
easier to navigate, and reducing the cost of adding more generated
modules relative to having to faff about with plugin hooks.

The `<sveltekit:generated>` prefix is bikesheddable, but I figured it's
worth being explicit about what this is, and using characters that are
invalid in npm package names.

Creating separate directories for dev and build means we don't need to
be as careful about what goes where, and can freely use relative imports
between generated modules. It means that building while also running a
dev server won't result in clobbering.

We can easily extend this to the other virtual modules.

---

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

- [x] 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
- [x] This message body should clearly illustrate what problems it
solves.
- [ ] Ideally, include a test that fails without this PR but passes with
it.

### Tests

- [x] 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

- [x] Please ensure that 'Allow edits from maintainers' is checked. PRs
without this option may be closed.

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Nic Polumeyv <nicolas.polum@outlook.com>
Co-authored-by: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace virtual modules with generated modules

2 participants