0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

fix: do not freeze process.env in dev (#12585)

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
This commit is contained in:
Florian Lefebvre 2024-12-09 13:29:44 +01:00 committed by GitHub
parent e21c7e67fd
commit a9373c0c9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 113 additions and 81 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes a case where `process.env` would be frozen despite changes made to environment variables in development

View file

@ -155,7 +155,6 @@ function createManifest(
i18n: manifest?.i18n,
checkOrigin: false,
middleware: manifest?.middleware ?? middlewareInstance,
envGetSecretEnabled: false,
key: createKey(),
};
}

View file

@ -69,7 +69,6 @@ export type SSRManifest = {
i18n: SSRManifestI18n | undefined;
middleware?: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance;
checkOrigin: boolean;
envGetSecretEnabled: boolean;
};
export type SSRManifestI18n = {

View file

@ -559,6 +559,5 @@ function createBuildManifest(
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
key,
envGetSecretEnabled: false,
};
}

View file

@ -274,8 +274,5 @@ function buildManifest(
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey,
envGetSecretEnabled:
(unwrapSupportKind(settings.adapter?.supportedAstroFeatures.envGetSecret) ??
'unsupported') !== 'unsupported',
};
}

View file

@ -41,6 +41,7 @@ import { vitePluginMiddleware } from './middleware/vite-plugin.js';
import { joinPaths } from './path.js';
import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js';
import { isObject } from './util.js';
import { createEnvLoader } from '../env/env-loader.js';
type CreateViteOptions = {
settings: AstroSettings;
@ -123,6 +124,7 @@ export async function createVite(
});
const srcDirPattern = glob.convertPathToPattern(fileURLToPath(settings.config.srcDir));
const envLoader = createEnvLoader();
// Start with the Vite configuration that Astro core needs
const commonConfig: vite.InlineConfig = {
@ -146,8 +148,8 @@ export async function createVite(
// 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.
command === 'dev' && vitePluginAstroServer({ settings, logger, fs, manifest, ssrManifest }), // ssrManifest is only required in dev mode, where it gets created before a Vite instance is created, and get passed to this function
envVitePlugin({ settings }),
astroEnv({ settings, mode, sync }),
envVitePlugin({ envLoader }),
astroEnv({ settings, mode, sync, envLoader }),
markdownVitePlugin({ settings, logger }),
htmlVitePlugin(),
astroPostprocessVitePlugin(),

60
packages/astro/src/env/env-loader.ts vendored Normal file
View file

@ -0,0 +1,60 @@
import { loadEnv } from 'vite';
import type { AstroConfig } from '../types/public/index.js';
import { fileURLToPath } from 'node:url';
// Match valid JS variable names (identifiers), which accepts most alphanumeric characters,
// except that the first character cannot be a number.
const isValidIdentifierRe = /^[_$a-zA-Z][\w$]*$/;
function getPrivateEnv(
fullEnv: Record<string, string>,
astroConfig: AstroConfig,
): Record<string, string> {
const viteConfig = astroConfig.vite;
let envPrefixes: string[] = ['PUBLIC_'];
if (viteConfig.envPrefix) {
envPrefixes = Array.isArray(viteConfig.envPrefix)
? viteConfig.envPrefix
: [viteConfig.envPrefix];
}
const privateEnv: Record<string, string> = {};
for (const key in fullEnv) {
// Ignore public env var
if (isValidIdentifierRe.test(key) && envPrefixes.every((prefix) => !key.startsWith(prefix))) {
if (typeof process.env[key] !== 'undefined') {
let value = process.env[key];
// Replacements are always strings, so try to convert to strings here first
if (typeof value !== 'string') {
value = `${value}`;
}
// Boolean values should be inlined to support `export const prerender`
// We already know that these are NOT sensitive values, so inlining is safe
if (value === '0' || value === '1' || value === 'true' || value === 'false') {
privateEnv[key] = value;
} else {
privateEnv[key] = `process.env.${key}`;
}
} else {
privateEnv[key] = JSON.stringify(fullEnv[key]);
}
}
}
return privateEnv;
}
export const createEnvLoader = () => {
let privateEnv: Record<string, string> = {};
return {
load: (mode: string, config: AstroConfig) => {
const loaded = loadEnv(mode, config.vite.envDir ?? fileURLToPath(config.root), '');
privateEnv = getPrivateEnv(loaded, config);
return loaded;
},
getPrivateEnv: () => {
return privateEnv;
},
};
};
export type EnvLoader = ReturnType<typeof createEnvLoader>;

View file

@ -4,14 +4,14 @@ import type { ValidationResultInvalid } from './validators.js';
export { validateEnvVariable, getEnvFieldType } from './validators.js';
export type GetEnv = (key: string) => string | undefined;
type OnSetGetEnv = (reset: boolean) => void;
type OnSetGetEnv = () => void;
let _getEnv: GetEnv = (key) => process.env[key];
export function setGetEnv(fn: GetEnv, reset = false) {
export function setGetEnv(fn: GetEnv) {
_getEnv = fn;
_onSetGetEnv(reset);
_onSetGetEnv();
}
let _onSetGetEnv: OnSetGetEnv = () => {};

View file

@ -1,6 +1,5 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { type Plugin, loadEnv } from 'vite';
import type { Plugin } from 'vite';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import type { AstroSettings } from '../types/astro.js';
import {
@ -11,28 +10,37 @@ import {
import { type InvalidVariable, invalidVariablesToError } from './errors.js';
import type { EnvSchema } from './schema.js';
import { getEnvFieldType, validateEnvVariable } from './validators.js';
import type { EnvLoader } from './env-loader.js';
interface AstroEnvPluginParams {
settings: AstroSettings;
mode: string;
sync: boolean;
envLoader: EnvLoader;
}
export function astroEnv({ settings, mode, sync }: AstroEnvPluginParams): Plugin {
export function astroEnv({ settings, mode, sync, envLoader }: AstroEnvPluginParams): Plugin {
const { schema, validateSecrets } = settings.config.env;
let isDev: boolean;
let templates: { client: string; server: string; internal: string } | null = null;
return {
name: 'astro-env-plugin',
enforce: 'pre',
config(_, { command }) {
isDev = command !== 'build';
},
buildStart() {
const loadedEnv = loadEnv(mode, fileURLToPath(settings.config.root), '');
const loadedEnv = envLoader.load(mode, settings.config);
if (!isDev) {
for (const [key, value] of Object.entries(loadedEnv)) {
if (value !== undefined) {
process.env[key] = value;
}
}
}
const validatedVariables = validatePublicVariables({
schema,
@ -42,7 +50,7 @@ export function astroEnv({ settings, mode, sync }: AstroEnvPluginParams): Plugin
});
templates = {
...getTemplates(schema, validatedVariables),
...getTemplates(schema, validatedVariables, isDev ? loadedEnv : null),
internal: `export const schema = ${JSON.stringify(schema)};`,
};
},
@ -122,6 +130,7 @@ function validatePublicVariables({
function getTemplates(
schema: EnvSchema,
validatedVariables: ReturnType<typeof validatePublicVariables>,
loadedEnv: Record<string, string> | null,
) {
let client = '';
let server = readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
@ -142,10 +151,15 @@ function getTemplates(
}
server += `export let ${key} = _internalGetSecret(${JSON.stringify(key)});\n`;
onSetGetEnv += `${key} = reset ? undefined : _internalGetSecret(${JSON.stringify(key)});\n`;
onSetGetEnv += `${key} = _internalGetSecret(${JSON.stringify(key)});\n`;
}
server = server.replace('// @@ON_SET_GET_ENV@@', onSetGetEnv);
if (loadedEnv) {
server = server.replace('// @@GET_ENV@@', `return (${JSON.stringify(loadedEnv)})[key];`);
} else {
server = server.replace('// @@GET_ENV@@', 'return _getEnv(key);');
}
return {
client,

View file

@ -191,7 +191,6 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
i18n: i18nManifest,
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
envGetSecretEnabled: false,
key: hasEnvironmentKey() ? getEnvironmentKey() : createKey(),
middleware() {
return {

View file

@ -1,63 +1,14 @@
import { fileURLToPath } from 'node:url';
import { transform } from 'esbuild';
import MagicString from 'magic-string';
import type * as vite from 'vite';
import { loadEnv } from 'vite';
import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
import type { EnvLoader } from '../env/env-loader.js';
interface EnvPluginOptions {
settings: AstroSettings;
envLoader: EnvLoader;
}
// Match `import.meta.env` directly without trailing property access
const importMetaEnvOnlyRe = /\bimport\.meta\.env\b(?!\.)/;
// Match valid JS variable names (identifiers), which accepts most alphanumeric characters,
// except that the first character cannot be a number.
const isValidIdentifierRe = /^[_$a-zA-Z][\w$]*$/;
function getPrivateEnv(
viteConfig: vite.ResolvedConfig,
astroConfig: AstroConfig,
): Record<string, string> {
let envPrefixes: string[] = ['PUBLIC_'];
if (viteConfig.envPrefix) {
envPrefixes = Array.isArray(viteConfig.envPrefix)
? viteConfig.envPrefix
: [viteConfig.envPrefix];
}
// Loads environment variables from `.env` files and `process.env`
const fullEnv = loadEnv(
viteConfig.mode,
viteConfig.envDir ?? fileURLToPath(astroConfig.root),
'',
);
const privateEnv: Record<string, string> = {};
for (const key in fullEnv) {
// Ignore public env var
if (isValidIdentifierRe.test(key) && envPrefixes.every((prefix) => !key.startsWith(prefix))) {
if (typeof process.env[key] !== 'undefined') {
let value = process.env[key];
// Replacements are always strings, so try to convert to strings here first
if (typeof value !== 'string') {
value = `${value}`;
}
// Boolean values should be inlined to support `export const prerender`
// We already know that these are NOT sensitive values, so inlining is safe
if (value === '0' || value === '1' || value === 'true' || value === 'false') {
privateEnv[key] = value;
} else {
privateEnv[key] = `process.env.${key}`;
}
} else {
privateEnv[key] = JSON.stringify(fullEnv[key]);
}
}
}
return privateEnv;
}
function getReferencedPrivateKeys(source: string, privateEnv: Record<string, any>): Set<string> {
const references = new Set<string>();
@ -114,13 +65,12 @@ async function replaceDefine(
};
}
export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plugin {
export default function envVitePlugin({ envLoader }: EnvPluginOptions): vite.Plugin {
let privateEnv: Record<string, string>;
let defaultDefines: Record<string, string>;
let isDev: boolean;
let devImportMetaEnvPrepend: string;
let viteConfig: vite.ResolvedConfig;
const { config: astroConfig } = settings;
return {
name: 'astro:vite-plugin-env',
config(_, { command }) {
@ -152,7 +102,9 @@ export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plug
}
// Find matches for *private* env and do our own replacement.
privateEnv ??= getPrivateEnv(viteConfig, astroConfig);
// Env is retrieved before process.env is populated by astro:env
// so that import.meta.env is first replaced by values, not process.env
privateEnv ??= envLoader.getPrivateEnv();
// In dev, we can assign the private env vars to `import.meta.env` directly for performance
if (isDev) {

View file

@ -2,14 +2,23 @@
import { schema } from 'virtual:astro:env/internal';
import {
createInvalidVariablesError,
getEnv,
getEnv as _getEnv,
getEnvFieldType,
setOnSetGetEnv,
validateEnvVariable,
} from 'astro/env/runtime';
// @ts-expect-error
/** @returns {string} */
// used while generating the virtual module
// biome-ignore lint/correctness/noUnusedFunctionParameters: `key` is used by the generated code
// biome-ignore lint/correctness/noUnusedVariables: `key` is used by the generated code
const getEnv = (key) => {
// @@GET_ENV@@
};
export const getSecret = (key) => {
return getEnv(key);
return getEnv(key)
};
const _internalGetSecret = (key) => {
@ -25,9 +34,6 @@ const _internalGetSecret = (key) => {
throw createInvalidVariablesError(key, type, result);
};
// used while generating the virtual module
// biome-ignore lint/correctness/noUnusedFunctionParameters: `reset` is used by the generated code
// biome-ignore lint/correctness/noUnusedVariables: `reset` is used by the generated code
setOnSetGetEnv((reset) => {
setOnSetGetEnv(() => {
// @@ON_SET_GET_ENV@@
});