Skip to content

Commit

Permalink
feat: Consistent handling of environment variables (#5663)
Browse files Browse the repository at this point in the history
* feat: Add env config object

* feat: Added types

* feat: Add .d.ts import to tsconfig

* feat: Generate env var dump code

* fix: imports

* fix: Remove old error mechanism

* feat: Rough edges of server-side module protection

* fix: Performance and add Vite overlay

* feat: First full draft of static environment vars

* fix unit tests

* fix: Rich is smarter than I am

* feat: Let  know about Vite mode

* feat: Added  to sync CLI

* feat: Added runtime variable support

* feat: Make server.init multicalls a noop

* feat: Set runtime env in preview

* this doesn't work?!: Set runtime env in dev

* feat: Added runtime env handler to cloudflare for testing

* Update packages/kit/src/cli.js

* Update packages/kit/src/core/sync/write_env.js

* Update packages/kit/src/core/sync/write_env.js

* Update packages/kit/src/vite/build/build_server.js

* Update packages/kit/src/core/sync/write_env.js

* make mode required

* move env/runtime to a file

* simplify write_env

* Update packages/kit/src/vite/preview/index.js

* remove unnecessary index.js suffix

* simplify

* tabs

* make init sync, make options required

* format

* cast process.env

* remove the overlay stuff, just throw an error

* just throw

* simplify traversal

* make build-time traversal more efficient

* move dev code into dev module

* always set env

* simplify

* remove hard-coded .svelte-kit

* rename env/runtime to env/platform and get it working in dev

* remove outdated comment

* docs

* App.RuntimeEnv -> App.Env

* add Env interface where necessary

* update more adapters

* changesets

* tweak changesets

* add a test

* replace doc links

* fix: Invalid js identifiers skipped

* rename modules

* fix some stuff, break some stuff

* update tests

* ok i think thats almost everything

* oops

* hide functions

* separate PrivateEnv from PublicEnv

* expose entry points

* fix turbo config

* fix

* Update .changeset/beige-gorillas-tell.md

* Update packages/kit/src/runtime/env-public.js

* Update packages/kit/src/runtime/env-private.js

* Update packages/kit/src/runtime/env-public.js

* Update packages/kit/src/runtime/env-private.js

* Update packages/kit/src/core/sync/sync.js

Co-authored-by: Ben McCann <[email protected]>

* Update packages/kit/src/vite/utils.js

Co-authored-by: Ben McCann <[email protected]>

* Update packages/kit/src/vite/utils.js

Co-authored-by: Ben McCann <[email protected]>

* Update packages/kit/test/apps/basics/.env

Co-authored-by: Ben McCann <[email protected]>

* Update packages/kit/test/apps/basics/.gitignore

Co-authored-by: Ben McCann <[email protected]>

* write_env in init, not update

* lint

* simplify valid identifier check, allow exports of reserved words

* move types/ambient.d.ts to ambient.d.ts so it doesnt get nuked on update

* Update documentation/docs/15-configuration.md

Co-authored-by: Ben McCann <[email protected]>

* remove env vars FAQ

* feat: Testing for import analysis

* Update packages/kit/types/ambient.d.ts

* warn if env vars are invalid

* oops

* argh

* Update packages/kit/scripts/extract-types.js

Co-authored-by: Ben McCann <[email protected]>

* explain why we strip the origin

* Update packages/kit/types/ambient.d.ts

Co-authored-by: Ben McCann <[email protected]>

* temporarily disable

* Revert "temporarily disable"

This reverts commit c1a5d52.

* tidy up

* remove another unused import

* Update packages/kit/src/vite/utils.js

Co-authored-by: Ben McCann <[email protected]>

* Update packages/kit/src/vite/utils.js

Co-authored-by: Ben McCann <[email protected]>

* Update packages/kit/src/vite/utils.js

Co-authored-by: Ben McCann <[email protected]>

* Update packages/kit/src/vite/utils.js

Co-authored-by: Ben McCann <[email protected]>

* Update packages/kit/src/core/sync/write_env.js

Co-authored-by: Ben McCann <[email protected]>

Co-authored-by: Rich Harris <[email protected]>
Co-authored-by: Rich Harris <[email protected]>
Co-authored-by: Ben McCann <[email protected]>
  • Loading branch information
4 people authored Jul 26, 2022
1 parent a6b0f00 commit b1a4055
Show file tree
Hide file tree
Showing 47 changed files with 851 additions and 142 deletions.
9 changes: 9 additions & 0 deletions .changeset/beige-gorillas-tell.md
Original file line number Diff line number Diff line change
@@ -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
---

Initialise `env`
5 changes: 5 additions & 0 deletions .changeset/pretty-kings-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Add `$env/static/private`, `$env/static/public`, `$env/dynamic/private` and `$env/dynamic/public` modules
3 changes: 0 additions & 3 deletions documentation/docs/06-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ declare namespace App {
interface Locals {
user: User;
}
interface Platform {}
interface Session {}
interface Stuff {}
}

const getUserInformation: (cookie: string | null) => Promise<User>;
Expand Down
15 changes: 12 additions & 3 deletions documentation/docs/15-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ const config = {
// ...
}
},
moduleExtensions: ['.js', '.ts'],
env: {
publicPrefix: 'PUBLIC_'
},
files: {
assets: 'static',
hooks: 'src/hooks',
Expand All @@ -44,6 +46,7 @@ const config = {
parameter: '_method',
allowed: []
},
moduleExtensions: ['.js', '.ts'],
outDir: '.svelte-kit',
package: {
dir: 'package',
Expand Down Expand Up @@ -157,9 +160,11 @@ When pages are prerendered, the CSP header is added via a `<meta http-equiv>` ta
> Note that most [Svelte transitions](https://svelte.dev/tutorial/transition) work by creating an inline `<style>` element. If you use these in your app, you must either leave the `style-src` directive unspecified or add `unsafe-inline`.
### moduleExtensions
### env

An array of file extensions that SvelteKit will treat as modules. Files with extensions that match neither `config.extensions` nor `config.kit.moduleExtensions` will be ignored by the router.
Environment variable configuration:

- `publicPrefix` — a prefix that signals that an environment variable is safe to expose to client-side code. See [`$env/static/public`](/docs/modules#$env-static-public) and [`$env/dynamic/public`](/docs/modules#$env-dynamic-public). Note that Vite's [`envPrefix`](https://vitejs.dev/config/shared-options.html#envprefix) must be set separately if you are using Vite's environment variable handling - though use of that feature should generally be unnecessary.

### files

Expand All @@ -186,6 +191,10 @@ See [HTTP Method Overrides](/docs/routing#endpoints-http-method-overrides). An o
- `parameter` — query parameter name to use for passing the intended method value
- `allowed` - array of HTTP methods that can be used when overriding the original request method

### moduleExtensions

An array of file extensions that SvelteKit will treat as modules. Files with extensions that match neither `config.extensions` nor `config.kit.moduleExtensions` will be ignored by the router.

### outDir

The directory that SvelteKit writes files to during `dev` and `build`. You should exclude this directory from version control.
Expand Down
9 changes: 0 additions & 9 deletions documentation/faq/60-env-vars.md

This file was deleted.

File renamed without changes.
2 changes: 2 additions & 0 deletions packages/adapter-cloudflare-workers/files/entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export default {
* @param {any} context
*/
async fetch(req, env, context) {
server.init({ env });

const url = new URL(req.url);

// static assets
Expand Down
1 change: 1 addition & 0 deletions packages/adapter-cloudflare/src/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const prefix = `/${manifest.appDir}/`;
/** @type {import('worktop/cfw').Module.Worker<{ ASSETS: import('worktop/cfw.durable').Durable.Object }>} */
const worker = {
async fetch(req, env, context) {
server.init({ env });
// skip cache if "cache-control: no-cache" in request
let pragma = req.headers.get('cache-control') || '';
let res = !pragma.includes('no-cache') && (await Cache.lookup(req));
Expand Down
4 changes: 4 additions & 0 deletions packages/adapter-netlify/src/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { split_headers } from './headers';
export function init(manifest) {
const server = new Server(manifest);

server.init({
env: process.env
});

return async (event, context) => {
const response = await server.respond(to_request(event), {
platform: { context },
Expand Down
1 change: 1 addition & 0 deletions packages/adapter-node/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { env } from './env.js';
/* global ENV_PREFIX */

const server = new Server(manifest);
server.init({ env: process.env });
const origin = env('ORIGIN', undefined);
const xff_depth = parseInt(env('XFF_DEPTH', '1'));

Expand Down
4 changes: 4 additions & 0 deletions packages/adapter-vercel/files/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ installPolyfills();

const server = new Server(manifest);

server.init({
env: process.env
});

/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
Expand Down
4 changes: 4 additions & 0 deletions packages/create-svelte/templates/default/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ declare namespace App {

// interface Platform {}

// interface PrivateEnv {}

// interface PublicEnv {}

// interface Session {}

// interface Stuff {}
Expand Down
2 changes: 2 additions & 0 deletions packages/create-svelte/templates/skeleton/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
declare namespace App {
// interface Locals {}
// interface Platform {}
// interface PrivateEnv {}
// interface PublicEnv {}
// interface Session {}
// interface Stuff {}
}
4 changes: 4 additions & 0 deletions packages/kit/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default [
'app/stores': 'src/runtime/app/stores.js',
'app/paths': 'src/runtime/app/paths.js',
'app/env': 'src/runtime/app/env.js',
'env/dynamic/private': 'src/runtime/env/dynamic/private.js',
'env/dynamic/public': 'src/runtime/env/dynamic/public.js',
'env-private': 'src/runtime/env-private.js',
'env-public': 'src/runtime/env-public.js',
paths: 'src/runtime/paths.js',
env: 'src/runtime/env.js'
},
Expand Down
133 changes: 77 additions & 56 deletions packages/kit/scripts/extract-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import fs from 'fs';
import ts from 'typescript';
import prettier from 'prettier';
import { mkdirp } from '../src/utils/filesystem.js';
import { fileURLToPath } from 'url';

/** @typedef {{ name: string, comment: string, snippet: string }} Extracted */

/** @type {Array<{ name: string, comment: string, exports: Extracted[], types: Extracted[] }>} */
/** @type {Array<{ name: string, comment: string, exports: Extracted[], types: Extracted[], exempt?: boolean }>} */
const modules = [];

/**
Expand All @@ -19,59 +20,72 @@ function get_types(code, statements) {
/** @type {Extracted[]} */
const types = [];

for (const statement of statements) {
if (
ts.isClassDeclaration(statement) ||
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement) ||
ts.isModuleDeclaration(statement) ||
ts.isVariableStatement(statement) ||
ts.isFunctionDeclaration(statement)
) {
const name_node = ts.isVariableStatement(statement)
? statement.declarationList.declarations[0]
: statement;

// @ts-ignore no idea why it's complaining here
const name = name_node.name?.escapedText;

let start = statement.pos;
let comment = '';

// @ts-ignore i think typescript is bad at typescript
if (statement.jsDoc) {
// @ts-ignore
comment = statement.jsDoc[0].comment;
// @ts-ignore
start = statement.jsDoc[0].end;
if (statements) {
for (const statement of statements) {
if (
ts.isClassDeclaration(statement) ||
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement) ||
ts.isModuleDeclaration(statement) ||
ts.isVariableStatement(statement) ||
ts.isFunctionDeclaration(statement)
) {
const name_node = ts.isVariableStatement(statement)
? statement.declarationList.declarations[0]
: statement;

// @ts-ignore no idea why it's complaining here
const name = name_node.name?.escapedText;

let start = statement.pos;
let comment = '';

// @ts-ignore i think typescript is bad at typescript
if (statement.jsDoc) {
// @ts-ignore
comment = statement.jsDoc[0].comment;
// @ts-ignore
start = statement.jsDoc[0].end;
}

const i = code.indexOf('export', start);
start = i + 6;

const snippet = prettier.format(code.slice(start, statement.end).trim(), {
parser: 'typescript',
printWidth: 80,
useTabs: true,
singleQuote: true,
trailingComma: 'none'
});

const collection =
ts.isVariableStatement(statement) || ts.isFunctionDeclaration(statement)
? exports
: types;

collection.push({ name, comment, snippet });
}

const i = code.indexOf('export', start);
start = i + 6;

const snippet = prettier.format(code.slice(start, statement.end).trim(), {
parser: 'typescript',
printWidth: 80,
useTabs: true,
singleQuote: true,
trailingComma: 'none'
});

const collection =
ts.isVariableStatement(statement) || ts.isFunctionDeclaration(statement) ? exports : types;

collection.push({ name, comment, snippet });
} else {
// console.log(statement.kind);
}
}

types.sort((a, b) => (a.name < b.name ? -1 : 1));
exports.sort((a, b) => (a.name < b.name ? -1 : 1));
types.sort((a, b) => (a.name < b.name ? -1 : 1));
exports.sort((a, b) => (a.name < b.name ? -1 : 1));
}

return { types, exports };
}

/**
* Type declarations include fully qualified URLs so that they become links when
* you hover over names in an editor with TypeScript enabled. We need to remove
* the origin so that they become root-relative, so that they work in preview
* deployments and when developing locally
* @param {string} str
*/
function strip_origin(str) {
return str.replace(/https:\/\/kit\.svelte\.dev/g, '');
}

{
const code = fs.readFileSync('types/index.d.ts', 'utf-8');
const node = ts.createSourceFile('index.d.ts', code, ts.ScriptTarget.Latest);
Expand All @@ -95,13 +109,20 @@ function get_types(code, statements) {
});
}

modules.push({
name: '$lib',
comment:
'This is a simple alias to `src/lib`, or whatever directory is specified as [`config.kit.files.lib`](/docs/configuration#files). It allows you to access common components and utility modules without `../../../../` nonsense.',
exports: [],
types: []
});
const dir = fileURLToPath(new URL('./special-types', import.meta.url).href);
for (const file of fs.readdirSync(dir)) {
if (!file.endsWith('.md')) continue;

const comment = strip_origin(fs.readFileSync(`${dir}/${file}`, 'utf-8'));

modules.push({
name: file.replace(/\+/g, '/').slice(0, -3),
comment,
exports: [],
types: [],
exempt: true
});
}

{
const code = fs.readFileSync('types/ambient.d.ts', 'utf-8');
Expand All @@ -113,13 +134,13 @@ modules.push({
const name = statement.name.text || statement.name.escapedText;

// @ts-ignore
const comment = statement.jsDoc?.[0].comment ?? '';
const comment = strip_origin(statement.jsDoc?.[0].comment ?? '');

modules.push({
name,
comment,
// @ts-ignore
...get_types(code, statement.body.statements)
...get_types(code, statement.body?.statements)
});
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/kit/scripts/special-types/$env+static+private.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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-platform), this module cannot be imported into client-side code.

_Unlike_ [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-platform), the values exported from this module are statically injected into your bundle at build time, enabling optimisations like dead code elimination.

```ts
import { API_KEY } from '$env/static/private';
```
7 changes: 7 additions & 0 deletions packages/kit/scripts/special-types/$env+static+public.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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.

Values are replaced statically at build time.

```ts
import { PUBLIC_BASE_URL } from '$env/static/public';
```
1 change: 1 addition & 0 deletions packages/kit/scripts/special-types/$lib.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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.
5 changes: 3 additions & 2 deletions packages/kit/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ prog
prog
.command('sync')
.describe('Synchronise generated files')
.action(async () => {
.option('--mode', 'Specify a mode for loading environment variables', 'development')
.action(async ({ mode }) => {
if (!fs.existsSync('svelte.config.js')) {
console.warn('Missing svelte.config.js — skipping');
return;
Expand All @@ -47,7 +48,7 @@ prog
try {
const config = await load_config();
const sync = await import('./core/sync/sync.js');
sync.all(config);
sync.all(config, mode);
} catch (error) {
handle_error(error);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const get_defaults = (prefix = '') => ({
reportOnly: directive_defaults
},
endpointExtensions: undefined,
env: {
publicPrefix: 'PUBLIC_'
},
files: {
assets: join(prefix, 'static'),
hooks: join(prefix, 'src/hooks'),
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ const options = object(
(keypath) => `${keypath} has been renamed to config.kit.moduleExtensions`
),

env: object({
publicPrefix: string('PUBLIC_')
}),

files: object({
assets: string('static'),
hooks: string(join('src', 'hooks')),
Expand Down
Loading

0 comments on commit b1a4055

Please sign in to comment.