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, i18n: manifest?.i18n,
checkOrigin: false, checkOrigin: false,
middleware: manifest?.middleware ?? middlewareInstance, middleware: manifest?.middleware ?? middlewareInstance,
envGetSecretEnabled: false,
key: createKey(), key: createKey(),
}; };
} }

View file

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

View file

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

View file

@ -274,8 +274,5 @@ function buildManifest(
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false, (settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
serverIslandNameMap: Array.from(settings.serverIslandNameMap), serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey, 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 { joinPaths } from './path.js';
import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js'; import { vitePluginServerIslands } from './server-islands/vite-plugin-server-islands.js';
import { isObject } from './util.js'; import { isObject } from './util.js';
import { createEnvLoader } from '../env/env-loader.js';
type CreateViteOptions = { type CreateViteOptions = {
settings: AstroSettings; settings: AstroSettings;
@ -123,6 +124,7 @@ export async function createVite(
}); });
const srcDirPattern = glob.convertPathToPattern(fileURLToPath(settings.config.srcDir)); const srcDirPattern = glob.convertPathToPattern(fileURLToPath(settings.config.srcDir));
const envLoader = createEnvLoader();
// Start with the Vite configuration that Astro core needs // Start with the Vite configuration that Astro core needs
const commonConfig: vite.InlineConfig = { 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 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. // 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 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 }), envVitePlugin({ envLoader }),
astroEnv({ settings, mode, sync }), astroEnv({ settings, mode, sync, envLoader }),
markdownVitePlugin({ settings, logger }), markdownVitePlugin({ settings, logger }),
htmlVitePlugin(), htmlVitePlugin(),
astroPostprocessVitePlugin(), 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 { validateEnvVariable, getEnvFieldType } from './validators.js';
export type GetEnv = (key: string) => string | undefined; export type GetEnv = (key: string) => string | undefined;
type OnSetGetEnv = (reset: boolean) => void; type OnSetGetEnv = () => void;
let _getEnv: GetEnv = (key) => process.env[key]; let _getEnv: GetEnv = (key) => process.env[key];
export function setGetEnv(fn: GetEnv, reset = false) { export function setGetEnv(fn: GetEnv) {
_getEnv = fn; _getEnv = fn;
_onSetGetEnv(reset); _onSetGetEnv();
} }
let _onSetGetEnv: OnSetGetEnv = () => {}; let _onSetGetEnv: OnSetGetEnv = () => {};

View file

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

View file

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

View file

@ -1,63 +1,14 @@
import { fileURLToPath } from 'node:url';
import { transform } from 'esbuild'; import { transform } from 'esbuild';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import type * as vite from 'vite'; import type * as vite from 'vite';
import { loadEnv } from 'vite'; import type { EnvLoader } from '../env/env-loader.js';
import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
interface EnvPluginOptions { interface EnvPluginOptions {
settings: AstroSettings; envLoader: EnvLoader;
} }
// Match `import.meta.env` directly without trailing property access // Match `import.meta.env` directly without trailing property access
const importMetaEnvOnlyRe = /\bimport\.meta\.env\b(?!\.)/; 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> { function getReferencedPrivateKeys(source: string, privateEnv: Record<string, any>): Set<string> {
const references = new 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 privateEnv: Record<string, string>;
let defaultDefines: Record<string, string>; let defaultDefines: Record<string, string>;
let isDev: boolean; let isDev: boolean;
let devImportMetaEnvPrepend: string; let devImportMetaEnvPrepend: string;
let viteConfig: vite.ResolvedConfig; let viteConfig: vite.ResolvedConfig;
const { config: astroConfig } = settings;
return { return {
name: 'astro:vite-plugin-env', name: 'astro:vite-plugin-env',
config(_, { command }) { config(_, { command }) {
@ -152,7 +102,9 @@ export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plug
} }
// Find matches for *private* env and do our own replacement. // 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 // In dev, we can assign the private env vars to `import.meta.env` directly for performance
if (isDev) { if (isDev) {

View file

@ -2,14 +2,23 @@
import { schema } from 'virtual:astro:env/internal'; import { schema } from 'virtual:astro:env/internal';
import { import {
createInvalidVariablesError, createInvalidVariablesError,
getEnv, getEnv as _getEnv,
getEnvFieldType, getEnvFieldType,
setOnSetGetEnv, setOnSetGetEnv,
validateEnvVariable, validateEnvVariable,
} from 'astro/env/runtime'; } 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) => { export const getSecret = (key) => {
return getEnv(key); return getEnv(key)
}; };
const _internalGetSecret = (key) => { const _internalGetSecret = (key) => {
@ -25,9 +34,6 @@ const _internalGetSecret = (key) => {
throw createInvalidVariablesError(key, type, result); throw createInvalidVariablesError(key, type, result);
}; };
// used while generating the virtual module setOnSetGetEnv(() => {
// 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) => {
// @@ON_SET_GET_ENV@@ // @@ON_SET_GET_ENV@@
}); });