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:
parent
37d4dd8d57
commit
39cbe50085
21 changed files with 248 additions and 6 deletions
9
.changeset/nine-donuts-confess.md
Normal file
9
.changeset/nine-donuts-confess.md
Normal 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
2
examples/env-vars/.env
Normal file
|
@ -0,0 +1,2 @@
|
|||
DB_PASSWORD=foobar
|
||||
PUBLIC_SOME_KEY=123
|
17
examples/env-vars/.gitignore
vendored
Normal file
17
examples/env-vars/.gitignore
vendored
Normal 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
2
examples/env-vars/.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
## force pnpm to hoist
|
||||
shamefully-hoist = true
|
6
examples/env-vars/.stackblitzrc
Normal file
6
examples/env-vars/.stackblitzrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"startCommand": "npm start",
|
||||
"env": {
|
||||
"ENABLE_CJS_IMPORTS": true
|
||||
}
|
||||
}
|
9
examples/env-vars/README.md
Normal file
9
examples/env-vars/README.md
Normal 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.
|
8
examples/env-vars/astro.config.mjs
Normal file
8
examples/env-vars/astro.config.mjs
Normal 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: [],
|
||||
});
|
14
examples/env-vars/package.json
Normal file
14
examples/env-vars/package.json
Normal 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"
|
||||
}
|
||||
}
|
BIN
examples/env-vars/public/favicon.ico
Normal file
BIN
examples/env-vars/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
11
examples/env-vars/sandbox.config.json
Normal file
11
examples/env-vars/sandbox.config.json
Normal 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
10
examples/env-vars/src/env.d.ts
vendored
Normal 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
|
||||
}
|
21
examples/env-vars/src/pages/index.astro
Normal file
21
examples/env-vars/src/pages/index.astro
Normal 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>
|
9
examples/env-vars/src/scripts/client.ts
Normal file
9
examples/env-vars/src/scripts/client.ts
Normal 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 });
|
||||
})()
|
6
examples/env-vars/tsconfig.json
Normal file
6
examples/env-vars/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"module": "ES2020"
|
||||
}
|
||||
}
|
|
@ -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 }),
|
||||
|
|
11
packages/astro/src/vite-plugin-env/README.md
Normal file
11
packages/astro/src/vite-plugin-env/README.md
Normal 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
|
||||
```
|
83
packages/astro/src/vite-plugin-env/index.ts
Normal file
83
packages/astro/src/vite-plugin-env/index.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
PUBLIC_PLACE: import.meta.env.PUBLIC_PLACE,
|
||||
SECRET_PLACE: import.meta.env.SECRET_PLACE,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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.`
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue