Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

breaking: serve public env dynamically, prevent use of dynamic env vars during prerendering #11277

Merged
merged 29 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ceb7503
create _env.js dynamic module
Rich-Harris Dec 12, 2023
bb540aa
tidy up
Rich-Harris Dec 12, 2023
ced5f32
tidy up
Rich-Harris Dec 12, 2023
18c0a9d
allow %sveltekit.env.PUBLIC_FOO% to bypass proxy
Rich-Harris Dec 12, 2023
c921db8
add config option
Rich-Harris Dec 12, 2023
f71120f
update test
Rich-Harris Dec 12, 2023
2f91cd6
docs
Rich-Harris Dec 12, 2023
20a2ca1
apply same restriction to dynamic/private
Rich-Harris Dec 12, 2023
ca45fa7
separate comments
Rich-Harris Dec 12, 2023
e36107f
drive-by: partially fix message
Rich-Harris Dec 12, 2023
48b7512
tweak
Rich-Harris Dec 12, 2023
d2ff6af
update test
Rich-Harris Dec 12, 2023
a132100
add test
Rich-Harris Dec 13, 2023
c129696
migration notes
Rich-Harris Dec 13, 2023
0f15ede
preload _env.js when appropriate
Rich-Harris Dec 13, 2023
050e113
fix adapter-static tests
Rich-Harris Dec 13, 2023
c438191
expose generateEnvModule method for adapter-static
Rich-Harris Dec 13, 2023
c336ee2
check
Rich-Harris Dec 13, 2023
115bd19
windows
Rich-Harris Dec 13, 2023
1197fcd
wow i really hate windows
Rich-Harris Dec 13, 2023
e5836a4
bump adapter-static peer dependency
Rich-Harris Dec 13, 2023
6bc7b8e
remove obsolete comment
Rich-Harris Dec 13, 2023
17fd0e0
changeset
Rich-Harris Dec 13, 2023
e87f14c
doh
Rich-Harris Dec 13, 2023
27b7aad
bump adapter-static as part of migration
Rich-Harris Dec 13, 2023
0337f6f
update migration docs
Rich-Harris Dec 13, 2023
3a0be04
reuse appDir instead of polluting everything
Rich-Harris Dec 13, 2023
400806a
merge
Rich-Harris Dec 13, 2023
d4f7ce2
regenerate types
Rich-Harris Dec 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-pugs-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: prevent use of dynamic env vars during prerendering, serve env vars dynamically
5 changes: 5 additions & 0 deletions .changeset/pink-lobsters-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-static': major
---

breaking: update SvelteKit peer dependency to version 2
20 changes: 19 additions & 1 deletion documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ As such, SvelteKit 2 replaces `resolvePath` with a (slightly better named) funct

`svelte-migrate` will do the method replacement for you, though if you later prepend the result with `base`, you need to remove that yourself.

## Dynamic environment variables cannot be used during prerendering

The `$env/dynamic/public` and `$env/dynamic/private` modules provide access to _run time_ environment variables, as opposed to the _build time_ environment variables exposed by `$env/static/public` and `$env/static/private`.

During prerendering in SvelteKit 1, they are one and the same. As such, prerendered pages that make use of 'dynamic' environment variables are really 'baking in' build time values, which is incorrect. Worse, `$env/dynamic/public` is populated in the browser with these stale values if the user happens to land on a prerendered page before navigating to dynamically-rendered pages.

Because of this, dynamic environment variables can no longer be read during prerendering in SvelteKit 2 — you should use the `static` modules instead. If the user lands on a prerendered page, SvelteKit will request up-to-date values for `$env/dynamic/public` from the server (by default from a module called `_env.js` — this can be configured with `config.kit.env.publicModule`) instead of reading them from the server-rendered HTML.
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved

## `form` and `data` have been removed from `use:enhance` callbacks

If you provide a callback to [`use:enhance`](/docs/form-actions#progressive-enhancement-use-enhance), it will be called with an object containing various useful properties.
Expand All @@ -117,6 +125,16 @@ If a form contains an `<input type="file">` but does not have an `enctype="multi

## Updated dependency requirements

SvelteKit requires Node `18.13` or higher, Vite `^5.0`, vite-plugin-svelte `^3.0`, TypeScript `^5.0` and Svelte version 4 or higher. `svelte-migrate` will do the `package.json` bumps for you.
SvelteKit 2 requires Node `18.13` or higher, and the following minimum dependency versions:

- `svelte@4`
- `vite@5`
- `typescript@5`
- `@sveltejs/adapter-static@3` (if you're using it)
- `@sveltejs/vite-plugin-svelte@3` (this is now required as a `peerDependency` of SvelteKit — previously it was directly depended upon)

`svelte-migrate` will update your `package.json` for you.

As part of the TypeScript upgrade, the generated `tsconfig.json` (the one your `tsconfig.json` extends from) now uses `"moduleResolution": "bundler"` (which is recommended by the TypeScript team, as it properly resolves types from packages with an `exports` map in package.json) and `verbatimModuleSyntax` (which replaces the existing `importsNotUsedAsValues ` and `preserveValueImports` flags — if you have those in your `tsconfig.json`, remove them. `svelte-migrate` will do this for you).

SvelteKit 2 uses ES2022 features, which are supported in all modern browsers.
1 change: 1 addition & 0 deletions packages/adapter-static/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ See https://kit.svelte.dev/docs/page-options#prerender for more details`
builder.rimraf(assets);
builder.rimraf(pages);

builder.generateEnvModule();
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
builder.writeClient(assets);
builder.writePrerendered(pages);

Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-static/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@
"vite": "^5.0.8"
},
"peerDependencies": {
"@sveltejs/kit": "^1.5.0 || ^2.0.0"
"@sveltejs/kit": "^2.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<script>
import { browser } from '$app/environment';
import { PUBLIC_ANSWER } from '$env/static/public';
import { env } from '$env/dynamic/public';
</script>

<h1>The answer is {env.PUBLIC_ANSWER}</h1>
<h1>The answer is {PUBLIC_ANSWER}</h1>

{#if browser}
<h2>The dynamic answer is {env.PUBLIC_ANSWER}</h2>
{/if}
1 change: 1 addition & 0 deletions packages/adapter-static/test/apps/prerendered/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ test('prerenders a referenced endpoint with implicit `prerender` setting', async
test('exposes public env vars to the client', async ({ page }) => {
await page.goto('/public-env');
expect(await page.textContent('h1')).toEqual('The answer is 42');
expect(await page.textContent('h2')).toEqual('The dynamic answer is 42');
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script>
import { env } from '$env/dynamic/public';
import { PUBLIC_VALUE } from '$env/static/public';
</script>

<h1>the fallback page was rendered</h1>

<b>{env.PUBLIC_VALUE}</b>
<b>{PUBLIC_VALUE}</b>
7 changes: 7 additions & 0 deletions packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ export function create_builder({
write(dest, fallback);
},

generateEnvModule() {
const dest = `${config.kit.outDir}/output/prerendered/dependencies/${config.kit.appDir}/env.js`;
const env = get_env(config.kit.env, vite_config.mode);

write(dest, `export const env=${JSON.stringify(env.public)}`);
},

generateManifest({ relativePath, routes: subset }) {
return generate_manifest({
build_data,
Expand Down
7 changes: 5 additions & 2 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ async function analyse({ manifest_path, env }) {

// set env, in case it's used in initialisation
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
internal.set_private_env(filter_private_env(env, { public_prefix, private_prefix }));
internal.set_public_env(filter_public_env(env, { public_prefix, private_prefix }));
const private_env = filter_private_env(env, { public_prefix, private_prefix });
const public_env = filter_public_env(env, { public_prefix, private_prefix });
internal.set_private_env(private_env);
internal.set_public_env(public_env);
internal.set_safe_public_env(public_env);

/** @type {import('types').ServerMetadata} */
const metadata = {
Expand Down
7 changes: 4 additions & 3 deletions packages/kit/src/core/postbuild/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
}

const files = new Set(walk(`${out}/client`).map(posixify));
files.add(`${config.appDir}/env.js`);

const immutable = `${config.appDir}/immutable`;
if (existsSync(`${out}/server/${immutable}`)) {
Expand Down Expand Up @@ -473,10 +474,10 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
}

if (not_prerendered.length > 0) {
const list = not_prerendered.map((id) => ` - ${id}`).join('\n');

throw new Error(
`The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:\n${not_prerendered.map(
(id) => ` - ${id}`
)}\n\nSee https://kit.svelte.dev/docs/page-options#prerender-troubleshooting for info on how to solve this`
`The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:\n${list}\n\nSee https://kit.svelte.dev/docs/page-options#prerender-troubleshooting for info on how to solve this`
);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ const server_template = ({
import root from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
import { set_building } from '__sveltekit/environment';
import { set_assets } from '__sveltekit/paths';
import { set_private_env, set_public_env } from '${runtime_directory}/shared-server.js';
import { set_private_env, set_public_env, set_safe_public_env } from '${runtime_directory}/shared-server.js';

export const options = {
app_dir: ${s(config.kit.appDir)},
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
csp: ${s(config.kit.csp)},
csrf_check_origin: ${s(config.kit.csrf.checkOrigin)},
Expand Down Expand Up @@ -62,7 +63,7 @@ export function get_hooks() {
return ${hooks ? `import(${s(hooks)})` : '{}'};
}

export { set_assets, set_building, set_private_env, set_public_env };
export { set_assets, set_building, set_private_env, set_public_env, set_safe_public_env };
`;

// TODO need to re-run this whenever src/app.html or src/error.html are
Expand Down
9 changes: 8 additions & 1 deletion packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export interface Builder {
*/
generateFallback(dest: string): Promise<void>;

/**
* Generate a module exposing build-time environment variables as `$env/dynamic/public`.
*/
generateEnvModule(): void;

/**
* Generate a server-side manifest to initialise the SvelteKit [server](https://kit.svelte.dev/docs/types#public-types-server) with.
* @param opts a relative path to the base directory of the app and optionally in which format (esm or cjs) the manifest should be generated
Expand Down Expand Up @@ -284,7 +289,9 @@ export interface KitConfig {
*/
alias?: Record<string, string>;
/**
* The directory relative to `paths.assets` where the built JS and CSS (and imported assets) are served from. (The filenames therein contain content-based hashes, meaning they can be cached indefinitely). Must not start or end with `/`.
* The directory where SvelteKit keeps its stuff, including static assets (such as JS and CSS) and internally-used routes.
*
* If `paths.assets` is specified, there will be two app directories — `${paths.assets}/${appDir}` and `${paths.base}/${appDir}`.
* @default "_app"
*/
appDir?: string;
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ export async function dev(vite, vite_config, svelte_config) {
app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`,
imports: [],
stylesheets: [],
fonts: []
fonts: [],
uses_env_dynamic_public: true
},
nodes: manifest_data.nodes.map((node, index) => {
return async () => {
Expand Down
6 changes: 2 additions & 4 deletions packages/kit/src/exports/vite/graph_analysis/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import path from 'node:path';
import { posixify } from '../../../utils/filesystem.js';
import { strip_virtual_prefix } from '../utils.js';
import { env_dynamic_private, env_static_private } from '../module_ids.js';

const ILLEGAL_IMPORTS = new Set([
'\0virtual:$env/dynamic/private',
'\0virtual:$env/static/private'
]);
const ILLEGAL_IMPORTS = new Set([env_dynamic_private, env_static_private]);
const ILLEGAL_MODULE_NAME_PATTERN = /.*\.server\..+/;

/**
Expand Down
32 changes: 22 additions & 10 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ import { s } from '../../utils/misc.js';
import { hash } from '../../runtime/hash.js';
import { dedent, isSvelte5Plus } from '../../core/sync/utils.js';
import sirv from 'sirv';
import {
env_dynamic_private,
env_dynamic_public,
env_static_private,
env_static_public,
service_worker,
sveltekit_environment,
sveltekit_paths
} from './module_ids.js';

export { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

Expand Down Expand Up @@ -365,35 +374,35 @@ function kit({ svelte_config }) {
}

switch (id) {
case '\0virtual:$env/static/private':
case env_static_private:
return create_static_module('$env/static/private', env.private);

case '\0virtual:$env/static/public':
case env_static_public:
return create_static_module('$env/static/public', env.public);

case '\0virtual:$env/dynamic/private':
case env_dynamic_private:
return create_dynamic_module(
'private',
vite_config_env.command === 'serve' ? env.private : undefined
);

case '\0virtual:$env/dynamic/public':
case env_dynamic_public:
// populate `$env/dynamic/public` from `window`
if (browser) {
return `export const env = ${global}.env;`;
return `export const env = ${global}.env ?? (await import(/* @vite-ignore */ ${global}.base + '/' + '${kit.appDir}/env.js')).env;`;
}

return create_dynamic_module(
'public',
vite_config_env.command === 'serve' ? env.public : undefined
);

case '\0virtual:$service-worker':
case service_worker:
return create_service_worker_module(svelte_config);

// for internal use only. it's published as $app/paths externally
// we use this alias so that we won't collide with user aliases
case '\0virtual:__sveltekit/paths': {
case sveltekit_paths: {
const { assets, base } = svelte_config.kit.paths;

// use the values defined in `global`, but fall back to hard-coded values
Expand Down Expand Up @@ -431,7 +440,7 @@ function kit({ svelte_config }) {
`;
}

case '\0virtual:__sveltekit/environment': {
case sveltekit_environment: {
const { version } = svelte_config.kit;

return dedent`
Expand Down Expand Up @@ -572,7 +581,7 @@ function kit({ svelte_config }) {
preserveEntrySignatures: 'strict'
},
ssrEmitAssets: true,
target: ssr ? 'node16.14' : undefined
target: ssr ? 'node18.13' : 'es2022'
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
},
publicDir: kit.files.assets,
worker: {
Expand Down Expand Up @@ -765,7 +774,10 @@ function kit({ svelte_config }) {
app: app.file,
imports: [...start.imports, ...app.imports],
stylesheets: [...start.stylesheets, ...app.stylesheets],
fonts: [...start.fonts, ...app.fonts]
fonts: [...start.fonts, ...app.fonts],
uses_env_dynamic_public: output.some(
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
)
};

const css = output.filter(
Expand Down
7 changes: 7 additions & 0 deletions packages/kit/src/exports/vite/module_ids.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const env_static_private = '\0virtual:$env/static/private';
export const env_static_public = '\0virtual:$env/static/public';
export const env_dynamic_private = '\0virtual:$env/dynamic/private';
export const env_dynamic_public = '\0virtual:$env/dynamic/public';
export const service_worker = '\0virtual:$service-worker';
export const sveltekit_paths = '\0virtual:__sveltekit/paths';
export const sveltekit_environment = '\0virtual:__sveltekit/environment';
29 changes: 29 additions & 0 deletions packages/kit/src/runtime/server/env_module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { public_env } from '../shared-server.js';

/** @type {string} */
let body;

/** @type {string} */
let etag;

/** @type {Headers} */
let headers;

/**
* @param {Request} request
* @returns {Response}
*/
export function get_public_env(request) {
body ??= `export const env=${JSON.stringify(public_env)}`;
etag ??= `W/${Date.now()}`;
headers ??= new Headers({
'content-type': 'application/javascript; charset=utf-8',
etag
});

if (request.headers.get('if-none-match') === etag) {
return new Response(undefined, { status: 304, headers });
}

return new Response(body, { headers });
}
36 changes: 23 additions & 13 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { respond } from './respond.js';
import { set_private_env, set_public_env } from '../shared-server.js';
import { set_private_env, set_public_env, set_safe_public_env } from '../shared-server.js';
import { options, get_hooks } from '__SERVER__/internal.js';
import { DEV } from 'esm-env';
import { filter_private_env, filter_public_env } from '../../utils/env.js';
import { building } from '../app/environment.js';

/** @type {ProxyHandler<{ type: 'public' | 'private' }>} */
const prerender_env_handler = {
get({ type }, prop) {
throw new Error(
`Cannot read values from $env/dynamic/${type} while prerendering (attempted to read env.${prop.toString()}). Use $env/static/${type} instead`
);
}
};

export class Server {
/** @type {import('types').SSROptions} */
Expand All @@ -27,19 +37,19 @@ export class Server {
// Take care: Some adapters may have to call `Server.init` per-request to set env vars,
// so anything that shouldn't be rerun should be wrapped in an `if` block to make sure it hasn't
// been done already.

// set env, in case it's used in initialisation
set_private_env(
filter_private_env(env, {
public_prefix: this.#options.env_public_prefix,
private_prefix: this.#options.env_private_prefix
})
);
set_public_env(
filter_public_env(env, {
public_prefix: this.#options.env_public_prefix,
private_prefix: this.#options.env_private_prefix
})
);
const prefixes = {
public_prefix: this.#options.env_public_prefix,
private_prefix: this.#options.env_private_prefix
};

const private_env = filter_private_env(env, prefixes);
const public_env = filter_public_env(env, prefixes);

set_private_env(building ? new Proxy({ type: 'private' }, prerender_env_handler) : private_env);
set_public_env(building ? new Proxy({ type: 'public' }, prerender_env_handler) : public_env);
set_safe_public_env(public_env);

if (!this.#options.hooks) {
try {
Expand Down
Loading
Loading