0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-30 22:03:56 -05:00

Expose private .env variables to import.meta.env during SSR (#2612)

* chore(examples): add env-vars example

* feat: improve import.meta.env support

* chore: add changeset

* test: update astro-envs test

* refactor: cleanup code based on feedback

* fix: import.meta guard

* fix: update memory test threshold to 10%
This commit is contained in:
Nate Moore 2022-02-18 16:06:56 -06:00 committed by GitHub
parent 37d4dd8d57
commit 39cbe50085
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 248 additions and 6 deletions

View file

@ -0,0 +1,9 @@
---
'astro': patch
---
Improve suppport for `import.meta.env`.
Prior to this change, all variables defined in `.env` files had to include the `PUBLIC_` prefix, meaning that they could potentially be visible to the client if referenced.
Now, Astro includes _any_ referenced variables defined in `.env` files on `import.meta.env` during server-side rendering, but only referenced `PUBLIC_` variables on the client.

2
examples/env-vars/.env Normal file
View file

@ -0,0 +1,2 @@
DB_PASSWORD=foobar
PUBLIC_SOME_KEY=123

17
examples/env-vars/.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# build output
dist
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

2
examples/env-vars/.npmrc Normal file
View file

@ -0,0 +1,2 @@
## force pnpm to hoist
shamefully-hoist = true

View file

@ -0,0 +1,6 @@
{
"startCommand": "npm start",
"env": {
"ENABLE_CJS_IMPORTS": true
}
}

View file

@ -0,0 +1,9 @@
# Astro Starter Kit: Environment Variables
```
npm init astro -- --template env-vars
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/env-vars)
This example showcases Astro's support for Environment Variables. Please see Vite's [Env Variables and Modes](https://vitejs.dev/guide/env-and-mode.html) guide for more information.

View file

@ -0,0 +1,8 @@
// Full Astro Configuration API Documentation:
// https://docs.astro.build/reference/configuration-reference
// @ts-check
export default /** @type {import('astro').AstroUserConfig} */ ({
// Comment out "renderers: []" to enable Astro's default component support.
renderers: [],
});

View file

@ -0,0 +1,14 @@
{
"name": "@example/env-vars",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"devDependencies": {
"astro": "^0.23.0-next.10"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,11 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"view": "browser",
"template": "node",
"container": {
"port": 3000,
"startScript": "start",
"node": "14"
}
}

10
examples/env-vars/src/env.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly DB_PASSWORD: string;
readonly PUBLIC_SOME_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View file

@ -0,0 +1,21 @@
---
const { SSR, DB_PASSWORD, PUBLIC_SOME_KEY } = import.meta.env;
// DB_PASSWORD is available because we're running on the server
console.log({ SSR, DB_PASSWORD });
// PUBLIC_SOME_KEY is available everywhere
console.log({ SSR, PUBLIC_SOME_KEY });
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Astro</title>
</head>
<body>
<h1>Hello, Environment Variables!</h1>
<script type="module" src="/src/scripts/client.ts"></script>
</body>
</html>

View file

@ -0,0 +1,9 @@
(() => {
const { SSR, DB_PASSWORD, PUBLIC_SOME_KEY } = import.meta.env;
// DB_PASSWORD is NOT available because we're running on the client
console.log({ SSR, DB_PASSWORD });
// PUBLIC_SOME_KEY is available everywhere
console.log({ SSR, PUBLIC_SOME_KEY });
})()

View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "ES2020"
}
}

View file

@ -10,6 +10,7 @@ import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.j
import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
import jsxVitePlugin from '../vite-plugin-jsx/index.js';
import envVitePlugin from '../vite-plugin-env/index.js';
import { resolveDependency } from './util.js';
// Some packages are just external, and thats the way it goes.
@ -54,6 +55,7 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig,
// The server plugin is for dev only and having it run during the build causes
// the build to run very slow as the filewatcher is triggered often.
mode === 'dev' && astroViteServerPlugin({ config: astroConfig, logging }),
envVitePlugin({ config: astroConfig }),
markdownVitePlugin({ config: astroConfig }),
jsxVitePlugin({ config: astroConfig, logging }),
astroPostprocessVitePlugin({ config: astroConfig }),

View file

@ -0,0 +1,11 @@
# vite-plugin-env
Improves Vite's [Env Variables](https://vitejs.dev/guide/env-and-mode.html#env-files) support to include **private** env variables during Server-Side Rendering (SSR) but never in client-side rendering (CSR).
Note that for added security, this plugin does not include **globally available env variable** that exist on `process.env`. It only loads variables defined in your local `.env` files.
Because of this, `MY_CLI_ARG` will never be added to `import.meta.env` during SSR or CSR.
```shell
MY_CLI_ARG=1 npm run dev
```

View file

@ -0,0 +1,83 @@
import type * as vite from 'vite';
import type { AstroConfig } from '../@types/astro';
import MagicString from 'magic-string';
import { fileURLToPath } from 'url';
import { loadEnv } from 'vite';
interface EnvPluginOptions {
config: AstroConfig;
}
function getPrivateEnv(viteConfig: vite.ResolvedConfig, astroConfig: AstroConfig) {
let envPrefixes: string[] = ['PUBLIC_'];
if (viteConfig.envPrefix) {
envPrefixes = Array.isArray(viteConfig.envPrefix) ? viteConfig.envPrefix : [viteConfig.envPrefix];
}
const fullEnv = loadEnv(viteConfig.mode, viteConfig.envDir ?? fileURLToPath(astroConfig.projectRoot), '');
const privateKeys = Object.keys(fullEnv).filter(key => {
// don't expose any variables also on `process.env`
// note: this filters out `CLI_ARGS=1` passed to node!
if (typeof process.env[key] !== 'undefined') return false;
// don't inject `PUBLIC_` variables, Vite handles that for us
for (const envPrefix of envPrefixes) {
if (key.startsWith(envPrefix)) return false;
}
// Otherwise, this is a private variable defined in an `.env` file
return true;
})
if (privateKeys.length === 0) {
return null;
}
return Object.fromEntries(privateKeys.map(key => [key, fullEnv[key]]));
}
function referencesPrivateKey(source: string, privateEnv: Record<string, any>) {
for (const key of Object.keys(privateEnv)) {
if (source.includes(key)) return true;
}
return false;
}
export default function envVitePlugin({ config: astroConfig }: EnvPluginOptions): vite.PluginOption {
let privateEnv: Record<string, any> | null;
let config: vite.ResolvedConfig;
return {
name: 'astro:vite-plugin-env',
enforce: 'pre',
configResolved(resolvedConfig) {
config = resolvedConfig;
if (config.envPrefix) {
}
},
async transform(source, id, options) {
const ssr = options?.ssr === true;
if (!ssr) return source;
if (!source.includes('import.meta')) return source;
if (!/\benv\b/.test(source)) return source;
if (typeof privateEnv === 'undefined') {
privateEnv = getPrivateEnv(config, astroConfig);
}
if (!privateEnv) return source;
if (!referencesPrivateKey(source, privateEnv)) return source;
const s = new MagicString(source);
// prettier-ignore
s.prepend(`import.meta.env = new Proxy(import.meta.env, {` +
`get(target, prop, reciever) {` +
`const PRIVATE = ${JSON.stringify(privateEnv)};` +
`if (typeof PRIVATE[prop] !== 'undefined') {` +
`return PRIVATE[prop];` +
`}` +
`return Reflect.get(target, prop, reciever);` +
`}` +
`});\n`);
return s.toString();
},
};
}

View file

@ -14,10 +14,10 @@ describe('Environment Variables', () => {
expect(true).to.equal(true);
});
it('does render public env, does not render private env', async () => {
it('does render public env and private env', async () => {
let indexHtml = await fixture.readFile('/index.html');
expect(indexHtml).to.not.include('CLUB_33');
expect(indexHtml).to.include('CLUB_33');
expect(indexHtml).to.include('BLUE_BAYOU');
});
@ -39,6 +39,27 @@ describe('Environment Variables', () => {
})
);
expect(found).to.equal(true, 'found the env variable in the JS build');
expect(found).to.equal(true, 'found the public env variable in the JS build');
});
it('does not include private env in client-side JS', async () => {
let dirs = await fixture.readdir('/assets');
let found = false;
// Look in all of the .js files to see if the public env is inlined.
// Testing this way prevents hardcoding expected js files.
// If we find it in any of them that's good enough to know its NOT working.
await Promise.all(
dirs.map(async (path) => {
if (path.endsWith('.js')) {
let js = await fixture.readFile(`/assets/${path}`);
if (js.includes('CLUB_33')) {
found = true;
}
}
})
);
expect(found).to.equal(false, 'found the private env variable in the JS build');
});
});

View file

@ -9,6 +9,7 @@ export default {
data() {
return {
PUBLIC_PLACE: import.meta.env.PUBLIC_PLACE,
SECRET_PLACE: import.meta.env.SECRET_PLACE,
};
},
};

View file

@ -3,4 +3,4 @@ import Client from '../components/Client.vue';
---
<environment-variable>{import.meta.env.PUBLIC_PLACE}</environment-variable>
<environment-variable>{import.meta.env.SECRET_PLACE}</environment-variable>
<Client client:load />
<Client client:load />

View file

@ -75,9 +75,9 @@ let medianOfAll = median(sizes);
// If the trailing average is higher than the median, see if it's more than 5% higher
if (averageOfLastThirty > medianOfAll) {
let percentage = Math.abs(averageOfLastThirty - medianOfAll) / medianOfAll;
if (percentage > 0.05) {
if (percentage > 0.1) {
throw new Error(
`The average towards the end (${prettyBytes(averageOfLastThirty)}) is more than 5% higher than the median of all runs (${prettyBytes(
`The average towards the end (${prettyBytes(averageOfLastThirty)}) is more than 10% higher than the median of all runs (${prettyBytes(
medianOfAll
)}). This tells us that memory continues to grow and a leak is likely.`
);