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

feat: Runtime SVELTE_PUBLIC_* env variables #4293

Closed

Conversation

bfanger
Copy link
Contributor

@bfanger bfanger commented Mar 11, 2022

Using environment variables in SvelteKit is possible via vite's VITE_* variables.

The downside is that these are compile-time environment variables, the values are injected into the javascript files during the dev and build steps.

This PR adds runtime environment variables, this allow you to change the variable in the environment without needing to recompile.

Use-cases:

  • Setting an environment variable and restarting the server works. This allows changing a Config Var on Heroku to activate instantly. (No longer needs a image rebuild & deploy)
  • Using an Azure Devops Build pipeline to create an Artifact and deploy that build to differrent environments using release pipelines.

Usage example:

import { env } from "$app/env";

console.log(env.SVELTE_PUBLIC_API_ENDPOINT);

How it works

At server startup a <script type="svelte/env"> tag is generated containing values from process.env that start with SVELTE_PUBLIC_.
When importing the $app/env the env export is populated with values from process.env on the server and on the client it uses the values from the 'svelte/env' tag.

Possible future improvements:

  • Add support for .env files. (Workaround node -r dotenv/config node_modules/.bin/svelte-kit dev)
  • Allow overriding the SVELTE_PUBLIC_ prefix/filter in the svelte.config
  • Allow adapters to provide enviroment variables. (process.env is a NodeJS api)
  • The SVELTE_PUBLIC_ prefix naming inspiration came from NEXT_PUBLIC_, but these are also compile-time variables (via webpack)

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 pnpx changeset and following the prompts. All changesets should be patch until SvelteKit 1.0

Related #3030
Alternative implementation using hooks.ts

@changeset-bot
Copy link

changeset-bot bot commented Mar 11, 2022

🦋 Changeset detected

Latest commit: b5cc17e

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

@Conduitry
Copy link
Member

Does process.env.* even work in non-Node environments?

I'm -1 on doing something like this. If people want to expose runtime variables to the client (of whichever kind are supported in their deployment environment), they can just do so manually in getSession.

@dominikg
Copy link
Member

This scenario and how it can be achieved is mentioned in the faq: https://kit.svelte.dev/faq#env-vars
and semi-recently here #3004 (comment)

first class support for it in kit - and if that is something we want to add at all - should have been discussed in a feature-request first.
Also the way this PR adds a script tag for the client is not safe, crafted env values could cause script injection, see #4128

@bfanger
Copy link
Contributor Author

bfanger commented Mar 11, 2022

Agree with the process.env argument (on the svelte-adapter-deno should use Deno.env.toObject() for example), any pointers on how to implement this with multiple runtimes in mind are welcome.

If a hacker can modify the env variables to generate executable javascript you are already lost, but thanks for the pointer to the render_json_payload_script utility.

I didn't consider getSession as I made assumptions it was like something PHP's $_SESSION and should be a place for data that depends on the user session. Diving deeper in how getSession is implemented and behaves in kit it's has the exact behavior needed for sharing the environment variables.

  • Works in adapter-static
  • Doen't result in additional fetch requests to sync the session when navigating.

getSession() is like a exposeTheseVariablesToClientOnce(serverRequest)

A slight advantage of this PR over getSession is that you can use import { env } from "$app/env" inside an endpoint.
(But as dotenv is not part of kit, its identical to process.env )

Feel free to treat my PR's as a feature-requests. it just has an example implementation as a bonus.

@benmccann benmccann closed this Mar 11, 2022
@Rich-Harris
Copy link
Member

Thanks for the PR — we definitely do need to come up with a consistent/abstracted approach to dealing with environment variables, since process.env isn't available in all platforms. Quite how we do that is very much up for debate — we need ticks in all four quadrants:

build time run time
public (client/server) ✅ import.meta.env.VITE_*
private (server-only) ⚠️ process.env.* (doesn't work everywhere)

And all this would ideally

  • work with .env and .env.* files
  • enable dead code elimination etc at build time
  • allow autocompletion/auto-imports
  • feel consistent across the four quadrants, but with clearly understandable distinctions between them (e.g. $app/env/public vs $app/env/private or whatever)

Aside: I've always found import.meta.env to be a deeply weird construct — it feels like we're just using the syntax that happens to be lying around, rather than designing something.

I'm not wholly convinced that getSession is the answer, since you're then restricted to using these values inside components. You can certainly imagine a module like this:

import { env } from '$app/env';
import { create_api } from '$lib/db';

export const api = create_api(env.PUBLIC_BASE_URL);

That said, it's not wholly clear to me what kinds of things would sit in that top right quadrant. Isn't that sort of thing generally known at build time?

I guess I should move this comment into a new issue.

@bfanger
Copy link
Contributor Author

bfanger commented Mar 12, 2022

I quickly ran into the Function called outside component initialization when trying to use the getSession approach.

I could use dependency injection and pass the $session.ENV as a parameter to the functions, but in my opinion that creates unnecessary boilerplate code.
Injecting the values into a global store/variable is also not a good idea, because that creates subtle bugs where the order of navigation matters.

So i'll keep using the workaround with the handle Hook, which i'll share here:

src/app.html
Change:

    %svelte.head%

Into

    %svelte.head%
    <script type="svelte/env"></script>

src/hooks.js

import dotenv from "dotenv";

dotenv.config();

const envScript = `<script type="svelte/env">${JSON.stringify(
  Object.fromEntries(
    Object.entries(process.env).filter(([key]) =>
      key.startsWith("SVELTE_PUBLIC_")
    )
  )
)}</script>`;

export async function handle({ event, resolve }) {
  const response = await resolve(event);
  const body = await response.text();
  return new Response(
    body.replace('<script type="svelte/env"></script>', envScript),
    response
  );
}

src/lib/env.js

let env = {};
if (typeof process === "object" && typeof process.env === "object") {
  env = process.env;
} else if (typeof document !== "undefined") {
  const el = document.querySelector('script[type="svelte/env"]');
  if (el) {
    env = JSON.parse(el.textContent);
  }
}
export default env;

@shiftlabs1
Copy link

@bfanger I just came across your proposed solution here while looking for the best way to deal with this issue. I found though that the first time my app runs, it fails as the variables are only loaded after the actual path resolves. these values become available only after a reload is done.

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.

6 participants