Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/gold-walls-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Change illegal import message to reference public-facing code rather than client-side code
3 changes: 3 additions & 0 deletions documentation/docs/01-project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -35,6 +37,7 @@ 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:
Expand Down
54 changes: 54 additions & 0 deletions documentation/docs/08-server-only-modules.md
Original file line number Diff line number Diff line change
@@ -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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/kit/scripts/special-types/$env+dynamic+private.md
Original file line number Diff line number Diff line change
@@ -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#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';
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/scripts/special-types/$env+static+private.md
Original file line number Diff line number Diff line change
@@ -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#env).
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.

Expand Down
4 changes: 4 additions & 0 deletions packages/kit/scripts/special-types/$lib.md
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 1 addition & 1 deletion packages/kit/src/exports/vite/graph_analysis/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class IllegalModuleGuard {
)
.join('\n');

return `Cannot import ${stack.at(-1)?.id} into client-side code:\n${pyramid}`;
return `Cannot import ${stack.at(-1)?.id} into public-facing code:\n${pyramid}`;
}
}

Expand Down
12 changes: 6 additions & 6 deletions packages/kit/src/exports/vite/graph_analysis/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe('IllegalImportGuard', (test) => {
});
assert.throws(
() => guard.assert_legal(module_graph),
/.*Cannot import \$env\/static\/private into client-side code:.*/gs
/.*Cannot import \$env\/static\/private into public-facing code:.*/gs
);
});

Expand All @@ -108,7 +108,7 @@ describe('IllegalImportGuard', (test) => {
});
assert.throws(
() => guard.assert_legal(module_graph),
/.*Cannot import \$env\/static\/private into client-side code:.*/gs
/.*Cannot import \$env\/static\/private into public-facing code:.*/gs
);
});

Expand All @@ -120,7 +120,7 @@ describe('IllegalImportGuard', (test) => {
});
assert.throws(
() => guard.assert_legal(module_graph),
/.*Cannot import \$env\/dynamic\/private into client-side code:.*/gs
/.*Cannot import \$env\/dynamic\/private into public-facing code:.*/gs
);
});

Expand All @@ -132,7 +132,7 @@ describe('IllegalImportGuard', (test) => {
});
assert.throws(
() => guard.assert_legal(module_graph),
/.*Cannot import \$env\/dynamic\/private into client-side code:.*/gs
/.*Cannot import \$env\/dynamic\/private into public-facing code:.*/gs
);
});

Expand All @@ -145,7 +145,7 @@ describe('IllegalImportGuard', (test) => {

assert.throws(
() => guard.assert_legal(module_graph),
/.*Cannot import \$lib\/test.server.js into client-side code:.*/gs
/.*Cannot import \$lib\/test.server.js into public-facing code:.*/gs
);
});

Expand All @@ -158,7 +158,7 @@ describe('IllegalImportGuard', (test) => {

assert.throws(
() => guard.assert_legal(module_graph),
/.*Cannot import \$lib\/server\/some\/nested\/path.js into client-side code:.*/gs
/.*Cannot import \$lib\/server\/some\/nested\/path.js into public-facing code:.*/gs
);
});

Expand Down
16 changes: 8 additions & 8 deletions packages/kit/test/apps/dev-only/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});

Expand All @@ -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
);
});
});
Expand All @@ -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
);
});
});
Expand All @@ -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
);
});
});
8 changes: 4 additions & 4 deletions packages/kit/test/build-errors/env.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});

Expand All @@ -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
);
});

Expand All @@ -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
);
});

Expand All @@ -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
);
});

Expand Down
8 changes: 4 additions & 4 deletions packages/kit/test/build-errors/server-only.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});

Expand All @@ -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
);
});

Expand All @@ -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
);
});

Expand All @@ -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
);
});

Expand Down