Skip to content

Commit

Permalink
feat(next): make astro:env stable (#11679)
Browse files Browse the repository at this point in the history
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
florian-lefebvre and sarah11918 authored Aug 21, 2024
1 parent 4cd6c43 commit ea71b90
Show file tree
Hide file tree
Showing 32 changed files with 210 additions and 282 deletions.
48 changes: 48 additions & 0 deletions .changeset/eighty-boxes-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
'astro': major
---

The `astro:env` feature introduced behind a flag in [v4.10.0](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md#x4100) is no longer experimental and is available for general use. If you have been waiting for stabilization before using `astro:env`, you can now do so.

This feature lets you configure a type-safe schema for your environment variables, and indicate whether they should be available on the server or the client.

To configure a schema, add the `env` option to your Astro config and define your client and server variables. If you were previously using this feature, please remove the experimental flag from your Astro config and move your entire `env` configuration unchanged to a top-level option.

```js
import { defineConfig, envField } from 'astro/config'

export default defineConfig({
env: {
schema: {
API_URL: envField.string({ context: "client", access: "public", optional: true }),
PORT: envField.number({ context: "server", access: "public", default: 4321 }),
API_SECRET: envField.string({ context: "server", access: "secret" }),
}
}
})
```

You can import and use your defined variables from the appropriate `/client` or `/server` module:

```astro
---
import { API_URL } from "astro:env/client"
import { API_SECRET_TOKEN } from "astro:env/server"
const data = await fetch(`${API_URL}/users`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_SECRET_TOKEN}`
},
})
---
<script>
import { API_URL } from "astro:env/client"
fetch(`${API_URL}/ping`)
</script>
```

Please see our [guide to using environment variables](https://docs.astro.build/en/guides/environment-variables/#astroenv) for more about this feature.
6 changes: 6 additions & 0 deletions .changeset/selfish-impalas-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/vercel': major
'@astrojs/node': major
---

Adds stable support for `astro:env`
1 change: 1 addition & 0 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="vite/types/import-meta.d.ts" />
/// <reference path="./types/content.d.ts" />
/// <reference path="./types/actions.d.ts" />
/// <reference path="./types/env.d.ts" />

interface ImportMetaEnv {
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ function createManifest(
i18n: manifest?.i18n,
checkOrigin: false,
middleware: manifest?.middleware ?? middleware ?? defaultMiddleware,
experimentalEnvGetSecretEnabled: false,
envGetSecretEnabled: false,
key: createKey(),
};
}
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ export type SSRManifest = {
i18n: SSRManifestI18n | undefined;
middleware: MiddlewareHandler;
checkOrigin: boolean;
// TODO: remove experimental prefix
experimentalEnvGetSecretEnabled: boolean;
envGetSecretEnabled: boolean;
};

export type SSRManifestI18n = {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export abstract class Pipeline {
}
// In SSR, getSecret should fail by default. Setting it here will run before the
// adapter override.
if (callSetGetEnv && manifest.experimentalEnvGetSecretEnabled) {
if (callSetGetEnv && manifest.envGetSecretEnabled) {
setGetEnv(() => {
throw new AstroError(AstroErrorData.EnvUnsupportedGetSecret);
}, true);
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,6 @@ function createBuildManifest(
middleware,
checkOrigin: settings.config.security?.checkOrigin ?? false,
key,
experimentalEnvGetSecretEnabled: false,
envGetSecretEnabled: false,
};
}
5 changes: 0 additions & 5 deletions packages/astro/src/core/build/plugins/plugin-chunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ export function vitePluginChunks(): VitePlugin {
if (id.includes('astro/dist/runtime')) {
return 'astro';
}
// Place `astro/env/setup` import in its own chunk to prevent Rollup's TLA bug
// https://github.com/rollup/rollup/issues/4708
if (id.includes('astro/dist/env/setup')) {
return 'astro/env-setup';
}
},
});
},
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,7 @@ function buildManifest(
checkOrigin: settings.config.security?.checkOrigin ?? false,
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey,
experimentalEnvGetSecretEnabled:
settings.config.experimental.env !== undefined &&
envGetSecretEnabled:
(settings.adapter?.supportedAstroFeatures.envGetSecret ?? 'unsupported') !== 'unsupported',
};
}
25 changes: 12 additions & 13 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export const ASTRO_CONFIG_DEFAULTS = {
legacy: {},
redirects: {},
security: {},
env: {
schema: {},
validateSecrets: false,
},
experimental: {
actions: false,
directRenderScript: false,
Expand All @@ -90,9 +94,6 @@ export const ASTRO_CONFIG_DEFAULTS = {
globalRoutePriority: false,
serverIslands: false,
contentIntellisense: false,
env: {
validateSecrets: false,
},
contentLayer: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };
Expand Down Expand Up @@ -503,6 +504,14 @@ export const AstroConfigSchema = z.object({
})
.optional()
.default(ASTRO_CONFIG_DEFAULTS.security),
env: z
.object({
schema: EnvSchema.optional().default(ASTRO_CONFIG_DEFAULTS.env.schema),
validateSecrets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.env.validateSecrets),
})
.strict()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.env),
experimental: z
.object({
actions: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.actions),
Expand All @@ -522,16 +531,6 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.globalRoutePriority),
env: z
.object({
schema: EnvSchema.optional(),
validateSecrets: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.env.validateSecrets),
})
.strict()
.optional(),
serverIslands: z
.boolean()
.optional()
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export async function createVite(
// the build to run very slow as the filewatcher is triggered often.
mode !== 'build' && vitePluginAstroServer({ settings, logger, fs }),
envVitePlugin({ settings, logger }),
astroEnv({ settings, mode, fs, sync }),
astroEnv({ settings, mode, sync }),
markdownVitePlugin({ settings, logger }),
htmlVitePlugin(),
astroPostprocessVitePlugin(),
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1235,15 +1235,15 @@ export const RouteNotFound = {
/**
* @docs
* @description
* Some environment variables do not match the data type and/or properties defined in `experimental.env.schema`.
* Some environment variables do not match the data type and/or properties defined in `env.schema`.
* @message
* The following environment variables defined in `experimental.env.schema` are invalid.
* The following environment variables defined in `env.schema` are invalid.
*/
export const EnvInvalidVariables = {
name: 'EnvInvalidVariables',
title: 'Invalid Environment Variables',
message: (errors: Array<string>) =>
`The following environment variables defined in \`experimental.env.schema\` are invalid:\n\n${errors.map((err) => `- ${err}`).join('\n')}\n`,
`The following environment variables defined in \`env.schema\` are invalid:\n\n${errors.map((err) => `- ${err}`).join('\n')}\n`,
} satisfies ErrorData;

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export async function syncInternal({
content: '',
});
}
syncAstroEnv(settings, fs);
syncAstroEnv(settings);

await writeFiles(settings, fs, logger);
logger.info('types', `Generated ${dim(getTimeStat(timerStart, performance.now()))}`);
Expand Down
5 changes: 2 additions & 3 deletions packages/astro/src/env/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ export const VIRTUAL_MODULES_IDS = {
};
export const VIRTUAL_MODULES_IDS_VALUES = new Set(Object.values(VIRTUAL_MODULES_IDS));

export const ENV_TYPES_FILE = 'env.d.ts';
export const ENV_TYPES_FILE = 'astro/env.d.ts';

const PKG_BASE = new URL('../../', import.meta.url);
export const MODULE_TEMPLATE_URL = new URL('templates/env/module.mjs', PKG_BASE);
export const TYPES_TEMPLATE_URL = new URL('templates/env/types.d.ts', PKG_BASE);
export const MODULE_TEMPLATE_URL = new URL('templates/env.mjs', PKG_BASE);
36 changes: 19 additions & 17 deletions packages/astro/src/env/sync.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
import fsMod from 'node:fs';
import type { AstroSettings } from '../types/astro.js';
import { TYPES_TEMPLATE_URL } from './constants.js';
import { ENV_TYPES_FILE } from './constants.js';
import { getEnvFieldType } from './validators.js';

export function syncAstroEnv(settings: AstroSettings, fs = fsMod): void {
if (!settings.config.experimental.env) {
return;
}

const schema = settings.config.experimental.env.schema ?? {};

export function syncAstroEnv(settings: AstroSettings): void {
let client = '';
let server = '';

for (const [key, options] of Object.entries(schema)) {
const str = `export const ${key}: ${getEnvFieldType(options)}; \n`;
for (const [key, options] of Object.entries(settings.config.env.schema)) {
const str = ` export const ${key}: ${getEnvFieldType(options)}; \n`;
if (options.context === 'client') {
client += str;
} else {
server += str;
}
}

const template = fs.readFileSync(TYPES_TEMPLATE_URL, 'utf-8');
const content = template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server);
let content = '';
if (client !== '') {
content = `declare module 'astro:env/client' {
${client}}`;
}
if (server !== '') {
content += `declare module 'astro:env/server' {
${server}}`;
}

settings.injectedTypes.push({
filename: 'astro/env.d.ts',
content,
});
if (content !== '') {
settings.injectedTypes.push({
filename: ENV_TYPES_FILE,
content,
});
}
}
30 changes: 7 additions & 23 deletions packages/astro/src/env/vite-plugin-env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type fsMod from 'node:fs';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { type Plugin, loadEnv } from 'vite';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
Expand All @@ -12,29 +12,14 @@ import { type InvalidVariable, invalidVariablesToError } from './errors.js';
import type { EnvSchema } from './schema.js';
import { getEnvFieldType, validateEnvVariable } from './validators.js';

// TODO: reminders for when astro:env comes out of experimental
// Types should always be generated (like in types/content.d.ts). That means the client module will be empty
// and server will only contain getSecret for unknown variables. Then, specifying a schema should only add
// variables as needed. For secret variables, it will only require specifying SecretValues and it should get
// merged with the static types/content.d.ts

interface AstroEnvVirtualModPluginParams {
interface AstroEnvPluginParams {
settings: AstroSettings;
mode: 'dev' | 'build' | string;
fs: typeof fsMod;
sync: boolean;
}

export function astroEnv({
settings,
mode,
fs,
sync,
}: AstroEnvVirtualModPluginParams): Plugin | undefined {
if (!settings.config.experimental.env) {
return;
}
const schema = settings.config.experimental.env.schema ?? {};
export function astroEnv({ settings, mode, sync }: AstroEnvPluginParams): Plugin {
const { schema, validateSecrets } = settings.config.env;

let templates: { client: string; server: string; internal: string } | null = null;

Expand All @@ -56,12 +41,12 @@ export function astroEnv({
const validatedVariables = validatePublicVariables({
schema,
loadedEnv,
validateSecrets: settings.config.experimental.env?.validateSecrets ?? false,
validateSecrets,
sync,
});

templates = {
...getTemplates(schema, fs, validatedVariables),
...getTemplates(schema, validatedVariables),
internal: `export const schema = ${JSON.stringify(schema)};`,
};
},
Expand Down Expand Up @@ -140,11 +125,10 @@ function validatePublicVariables({

function getTemplates(
schema: EnvSchema,
fs: typeof fsMod,
validatedVariables: ReturnType<typeof validatePublicVariables>,
) {
let client = '';
let server = fs.readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
let server = readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
let onSetGetEnv = '';

for (const { key, value, context } of validatedVariables) {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/integrations/features-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function validateSupportedFeatures(
adapterName,
logger,
'astro:env getSecret',
() => config?.experimental?.env !== undefined,
() => Object.keys(config?.env?.schema ?? {}).length !== 0,
);

return validationResult;
Expand Down
Loading

0 comments on commit ea71b90

Please sign in to comment.