diff --git a/.changeset/nine-donuts-confess.md b/.changeset/nine-donuts-confess.md
new file mode 100644
index 0000000000..7ae040c237
--- /dev/null
+++ b/.changeset/nine-donuts-confess.md
@@ -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.
diff --git a/examples/env-vars/.env b/examples/env-vars/.env
new file mode 100644
index 0000000000..dd89799f8f
--- /dev/null
+++ b/examples/env-vars/.env
@@ -0,0 +1,2 @@
+DB_PASSWORD=foobar
+PUBLIC_SOME_KEY=123
diff --git a/examples/env-vars/.gitignore b/examples/env-vars/.gitignore
new file mode 100644
index 0000000000..c824674530
--- /dev/null
+++ b/examples/env-vars/.gitignore
@@ -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
diff --git a/examples/env-vars/.npmrc b/examples/env-vars/.npmrc
new file mode 100644
index 0000000000..65922326b2
--- /dev/null
+++ b/examples/env-vars/.npmrc
@@ -0,0 +1,2 @@
+## force pnpm to hoist
+shamefully-hoist = true
diff --git a/examples/env-vars/.stackblitzrc b/examples/env-vars/.stackblitzrc
new file mode 100644
index 0000000000..43798ecff8
--- /dev/null
+++ b/examples/env-vars/.stackblitzrc
@@ -0,0 +1,6 @@
+{
+ "startCommand": "npm start",
+ "env": {
+ "ENABLE_CJS_IMPORTS": true
+ }
+}
\ No newline at end of file
diff --git a/examples/env-vars/README.md b/examples/env-vars/README.md
new file mode 100644
index 0000000000..686ccd77f1
--- /dev/null
+++ b/examples/env-vars/README.md
@@ -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.
diff --git a/examples/env-vars/astro.config.mjs b/examples/env-vars/astro.config.mjs
new file mode 100644
index 0000000000..67c95c2403
--- /dev/null
+++ b/examples/env-vars/astro.config.mjs
@@ -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: [],
+});
diff --git a/examples/env-vars/package.json b/examples/env-vars/package.json
new file mode 100644
index 0000000000..4a633216d5
--- /dev/null
+++ b/examples/env-vars/package.json
@@ -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"
+ }
+}
diff --git a/examples/env-vars/public/favicon.ico b/examples/env-vars/public/favicon.ico
new file mode 100644
index 0000000000..578ad458b8
Binary files /dev/null and b/examples/env-vars/public/favicon.ico differ
diff --git a/examples/env-vars/sandbox.config.json b/examples/env-vars/sandbox.config.json
new file mode 100644
index 0000000000..9178af77d7
--- /dev/null
+++ b/examples/env-vars/sandbox.config.json
@@ -0,0 +1,11 @@
+{
+ "infiniteLoopProtection": true,
+ "hardReloadOnChange": false,
+ "view": "browser",
+ "template": "node",
+ "container": {
+ "port": 3000,
+ "startScript": "start",
+ "node": "14"
+ }
+}
diff --git a/examples/env-vars/src/env.d.ts b/examples/env-vars/src/env.d.ts
new file mode 100644
index 0000000000..a1befd0f03
--- /dev/null
+++ b/examples/env-vars/src/env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly DB_PASSWORD: string;
+ readonly PUBLIC_SOME_KEY: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
diff --git a/examples/env-vars/src/pages/index.astro b/examples/env-vars/src/pages/index.astro
new file mode 100644
index 0000000000..0d19b9a46d
--- /dev/null
+++ b/examples/env-vars/src/pages/index.astro
@@ -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 });
+---
+
+
+
+
+
+ Astro
+
+
+ Hello, Environment Variables!
+
+
+
diff --git a/examples/env-vars/src/scripts/client.ts b/examples/env-vars/src/scripts/client.ts
new file mode 100644
index 0000000000..05961d3995
--- /dev/null
+++ b/examples/env-vars/src/scripts/client.ts
@@ -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 });
+})()
diff --git a/examples/env-vars/tsconfig.json b/examples/env-vars/tsconfig.json
new file mode 100644
index 0000000000..c9d2331c8d
--- /dev/null
+++ b/examples/env-vars/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "moduleResolution": "node",
+ "module": "ES2020"
+ }
+}
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index d7aaec990f..044e4852ea 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -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 that’s 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 }),
diff --git a/packages/astro/src/vite-plugin-env/README.md b/packages/astro/src/vite-plugin-env/README.md
new file mode 100644
index 0000000000..0e2a7d7d5c
--- /dev/null
+++ b/packages/astro/src/vite-plugin-env/README.md
@@ -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
+```
diff --git a/packages/astro/src/vite-plugin-env/index.ts b/packages/astro/src/vite-plugin-env/index.ts
new file mode 100644
index 0000000000..803b97a147
--- /dev/null
+++ b/packages/astro/src/vite-plugin-env/index.ts
@@ -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) {
+ 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 | 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();
+ },
+ };
+}
diff --git a/packages/astro/test/astro-envs.test.js b/packages/astro/test/astro-envs.test.js
index 56b0471da8..22a4502dc3 100644
--- a/packages/astro/test/astro-envs.test.js
+++ b/packages/astro/test/astro-envs.test.js
@@ -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');
});
});
diff --git a/packages/astro/test/fixtures/astro-envs/src/components/Client.vue b/packages/astro/test/fixtures/astro-envs/src/components/Client.vue
index 01bae708aa..7162c56329 100644
--- a/packages/astro/test/fixtures/astro-envs/src/components/Client.vue
+++ b/packages/astro/test/fixtures/astro-envs/src/components/Client.vue
@@ -9,6 +9,7 @@ export default {
data() {
return {
PUBLIC_PLACE: import.meta.env.PUBLIC_PLACE,
+ SECRET_PLACE: import.meta.env.SECRET_PLACE,
};
},
};
diff --git a/packages/astro/test/fixtures/astro-envs/src/pages/index.astro b/packages/astro/test/fixtures/astro-envs/src/pages/index.astro
index b2cb02b9d5..f71c11db75 100644
--- a/packages/astro/test/fixtures/astro-envs/src/pages/index.astro
+++ b/packages/astro/test/fixtures/astro-envs/src/pages/index.astro
@@ -3,4 +3,4 @@ import Client from '../components/Client.vue';
---
{import.meta.env.PUBLIC_PLACE}
{import.meta.env.SECRET_PLACE}
-
\ No newline at end of file
+
diff --git a/scripts/memory/index.js b/scripts/memory/index.js
index 2729c76311..29b20832dc 100644
--- a/scripts/memory/index.js
+++ b/scripts/memory/index.js
@@ -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.`
);