From 31a3b3a92c112c810b2e326f27ceb3ad4728e30f Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Mon, 12 Aug 2024 15:15:28 +0200 Subject: [PATCH] feat: work on types and codegen --- packages/astro/client.d.ts | 1 + packages/astro/src/@types/astro.ts | 284 +++++++++--------- packages/astro/src/core/config/schema.ts | 25 +- packages/astro/src/env/constants.ts | 3 +- packages/astro/src/env/sync.ts | 39 ++- packages/astro/src/env/vite-plugin-env.ts | 20 +- .../templates/{env/module.mjs => env.mjs} | 0 .../env/types.d.ts => types/env.d.ts} | 6 +- 8 files changed, 185 insertions(+), 193 deletions(-) rename packages/astro/templates/{env/module.mjs => env.mjs} (100%) rename packages/astro/{templates/env/types.d.ts => types/env.d.ts} (59%) diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts index ed5c1b894d..9775afa431 100644 --- a/packages/astro/client.d.ts +++ b/packages/astro/client.d.ts @@ -1,6 +1,7 @@ /// /// /// +/// // eslint-disable-next-line @typescript-eslint/no-namespace declare namespace App { diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 64923d91c9..b67ca9ba70 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1755,6 +1755,148 @@ export interface AstroUserConfig { */ legacy?: object; + /** + * @docs + * @name experimental.env + * @type {object} + * @default `undefined` + * @version 4.10.0 + * @description + * + * Enables experimental `astro:env` features. + * + * The `astro:env` API lets you configure a type-safe schema for your environment variables, and indicate whether they should be available on the server or the client. 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}` + * }, + * }) + * --- + * + * + * ``` + * + * To define the data type and properties of your environment variables, declare a schema in your Astro config in `experimental.env.schema`. The `envField` helper allows you define your variable as a string, number, or boolean and pass properties in an object: + * + * ```js + * // astro.config.mjs + * import { defineConfig, envField } from "astro/config" + * + * export default defineConfig({ + * experimental: { + * 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" }), + * } + * } + * } + * }) + * ``` + * + * There are currently four data types supported: strings, numbers, booleans and enums. + * + * There are three kinds of environment variables, determined by the combination of `context` (client or server) and `access` (secret or public) settings defined in your [`env.schema`](#experimentalenvschema): + * + * - **Public client variables**: These variables end up in both your final client and server bundles, and can be accessed from both client and server through the `astro:env/client` module: + * + * ```js + * import { API_URL } from "astro:env/client" + * ``` + * + * - **Public server variables**: These variables end up in your final server bundle and can be accessed on the server through the `astro:env/server` module: + * + * ```js + * import { PORT } from "astro:env/server" + * ``` + * + * - **Secret server variables**: These variables are not part of your final bundle and can be accessed on the server through the `astro:env/server` module. The `getSecret()` helper function can be used to retrieve secrets not specified in the schema. Its implementation is provided by your adapter and defaults to `process.env`: + * + * ```js + * import { API_SECRET, getSecret } from "astro:env/server" + * + * const SECRET_NOT_IN_SCHEMA = getSecret("SECRET_NOT_IN_SCHEMA") // string | undefined + * ``` + * + * **Note:** Secret client variables are not supported because there is no safe way to send this data to the client. Therefore, it is not possible to configure both `context: "client"` and `access: "secret"` in your schema. + * + * For a complete overview, and to give feedback on this experimental API, see the [Astro Env RFC](https://github.com/withastro/roadmap/blob/feat/astro-env-rfc/proposals/0046-astro-env.md). + */ + env?: { + /** + * @docs + * @name experimental.env.schema + * @kind h4 + * @type {EnvSchema} + * @default `undefined` + * @version 4.10.0 + * @description + * + * An object that uses `envField` to define the data type (`string`, `number`, or `boolean`) and properties of your environment variables: `context` (client or server), `access` (public or secret), a `default` value to use, and whether or not this environment variable is `optional` (defaults to `false`). + * ```js + * // astro.config.mjs + * import { defineConfig, envField } from "astro/config" + * + * export default defineConfig({ + * experimental: { + * 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" }), + * } + * } + * } + * }) + * ``` + */ + schema?: EnvSchema; + + /** + * @docs + * @name experimental.env.validateSecrets + * @kind h4 + * @type {boolean} + * @default `false` + * @version 4.11.6 + * @description + * + * Whether or not to validate secrets on the server when starting the dev server or running a build. + * + * By default, only public variables are validated on the server when starting the dev server or a build, and private variables are validated at runtime only. If enabled, private variables will also be checked on start. This is useful in some continuous integration (CI) pipelines to make sure all your secrets are correctly set before deploying. + * + * ```js + * // astro.config.mjs + * import { defineConfig, envField } from "astro/config" + * + * export default defineConfig({ + * experimental: { + * env: { + * schema: { + * // ... + * }, + * validateSecrets: true + * } + * } + * }) + * ``` + */ + validateSecrets?: boolean; + }; + /** * @docs * @kind heading @@ -1977,148 +2119,6 @@ export interface AstroUserConfig { */ globalRoutePriority?: boolean; - /** - * @docs - * @name experimental.env - * @type {object} - * @default `undefined` - * @version 4.10.0 - * @description - * - * Enables experimental `astro:env` features. - * - * The `astro:env` API lets you configure a type-safe schema for your environment variables, and indicate whether they should be available on the server or the client. 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}` - * }, - * }) - * --- - * - * - * ``` - * - * To define the data type and properties of your environment variables, declare a schema in your Astro config in `experimental.env.schema`. The `envField` helper allows you define your variable as a string, number, or boolean and pass properties in an object: - * - * ```js - * // astro.config.mjs - * import { defineConfig, envField } from "astro/config" - * - * export default defineConfig({ - * experimental: { - * 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" }), - * } - * } - * } - * }) - * ``` - * - * There are currently four data types supported: strings, numbers, booleans and enums. - * - * There are three kinds of environment variables, determined by the combination of `context` (client or server) and `access` (secret or public) settings defined in your [`env.schema`](#experimentalenvschema): - * - * - **Public client variables**: These variables end up in both your final client and server bundles, and can be accessed from both client and server through the `astro:env/client` module: - * - * ```js - * import { API_URL } from "astro:env/client" - * ``` - * - * - **Public server variables**: These variables end up in your final server bundle and can be accessed on the server through the `astro:env/server` module: - * - * ```js - * import { PORT } from "astro:env/server" - * ``` - * - * - **Secret server variables**: These variables are not part of your final bundle and can be accessed on the server through the `astro:env/server` module. The `getSecret()` helper function can be used to retrieve secrets not specified in the schema. Its implementation is provided by your adapter and defaults to `process.env`: - * - * ```js - * import { API_SECRET, getSecret } from "astro:env/server" - * - * const SECRET_NOT_IN_SCHEMA = getSecret("SECRET_NOT_IN_SCHEMA") // string | undefined - * ``` - * - * **Note:** Secret client variables are not supported because there is no safe way to send this data to the client. Therefore, it is not possible to configure both `context: "client"` and `access: "secret"` in your schema. - * - * For a complete overview, and to give feedback on this experimental API, see the [Astro Env RFC](https://github.com/withastro/roadmap/blob/feat/astro-env-rfc/proposals/0046-astro-env.md). - */ - env?: { - /** - * @docs - * @name experimental.env.schema - * @kind h4 - * @type {EnvSchema} - * @default `undefined` - * @version 4.10.0 - * @description - * - * An object that uses `envField` to define the data type (`string`, `number`, or `boolean`) and properties of your environment variables: `context` (client or server), `access` (public or secret), a `default` value to use, and whether or not this environment variable is `optional` (defaults to `false`). - * ```js - * // astro.config.mjs - * import { defineConfig, envField } from "astro/config" - * - * export default defineConfig({ - * experimental: { - * 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" }), - * } - * } - * } - * }) - * ``` - */ - schema?: EnvSchema; - - /** - * @docs - * @name experimental.env.validateSecrets - * @kind h4 - * @type {boolean} - * @default `false` - * @version 4.11.6 - * @description - * - * Whether or not to validate secrets on the server when starting the dev server or running a build. - * - * By default, only public variables are validated on the server when starting the dev server or a build, and private variables are validated at runtime only. If enabled, private variables will also be checked on start. This is useful in some continuous integration (CI) pipelines to make sure all your secrets are correctly set before deploying. - * - * ```js - * // astro.config.mjs - * import { defineConfig, envField } from "astro/config" - * - * export default defineConfig({ - * experimental: { - * env: { - * schema: { - * // ... - * }, - * validateSecrets: true - * } - * } - * }) - * ``` - */ - validateSecrets?: boolean; - }; - /** * @docs * @name experimental.serverIslands diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 9ffb58934b..313089182a 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -82,6 +82,10 @@ export const ASTRO_CONFIG_DEFAULTS = { legacy: {}, redirects: {}, security: {}, + env: { + schema: {}, + validateSecrets: false, + }, experimental: { actions: false, directRenderScript: false, @@ -89,9 +93,6 @@ export const ASTRO_CONFIG_DEFAULTS = { clientPrerender: false, globalRoutePriority: false, serverIslands: false, - env: { - validateSecrets: false, - }, }, } satisfies AstroUserConfig & { server: { open: boolean } }; @@ -505,6 +506,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), @@ -524,16 +533,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() diff --git a/packages/astro/src/env/constants.ts b/packages/astro/src/env/constants.ts index de5f06233d..220f63373c 100644 --- a/packages/astro/src/env/constants.ts +++ b/packages/astro/src/env/constants.ts @@ -8,5 +8,4 @@ export const VIRTUAL_MODULES_IDS_VALUES = new Set(Object.values(VIRTUAL_MODULES_ export const ENV_TYPES_FILE = '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); diff --git a/packages/astro/src/env/sync.ts b/packages/astro/src/env/sync.ts index 9ba11469ad..597d222b62 100644 --- a/packages/astro/src/env/sync.ts +++ b/packages/astro/src/env/sync.ts @@ -1,30 +1,37 @@ import fsMod from 'node:fs'; import type { AstroSettings } from '../@types/astro.js'; -import { ENV_TYPES_FILE, 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) { - if (!settings.config.experimental.env) { - return; - } + let client: string | null = null; + let server: string | null = null; - const schema = settings.config.experimental.env.schema ?? {}; - - 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 ??= ''; client += str; } else { + server ??= ''; server += str; } } - const template = fs.readFileSync(TYPES_TEMPLATE_URL, 'utf-8'); - const dts = template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server); - - fs.mkdirSync(settings.dotAstroDir, { recursive: true }); - fs.writeFileSync(new URL(ENV_TYPES_FILE, settings.dotAstroDir), dts, 'utf-8'); + let content: string | null = null; + if (client !== null) { + content = `declare module 'astro:env/client' { +${client} +}`; + } + if (server !== null) { + content ??= ''; + content += `declare module 'astro:env/server' { +${server} +}`; + } + if (content) { + fs.mkdirSync(settings.dotAstroDir, { recursive: true }); + fs.writeFileSync(new URL(ENV_TYPES_FILE, settings.dotAstroDir), content, 'utf-8'); + } } diff --git a/packages/astro/src/env/vite-plugin-env.ts b/packages/astro/src/env/vite-plugin-env.ts index 9a05d24e8a..06f34ae97d 100644 --- a/packages/astro/src/env/vite-plugin-env.ts +++ b/packages/astro/src/env/vite-plugin-env.ts @@ -12,32 +12,22 @@ 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 // TODO: rename experimentalWhatever in ssr manifest // TODO: update integrations compat // TODO: update adapters -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 || sync) { +export function astroEnv({ settings, mode, fs, sync }: AstroEnvPluginParams): Plugin | undefined { + if (sync) { return; } - const schema = settings.config.experimental.env.schema ?? {}; + const { schema, validateSecrets } = settings.config.env; let templates: { client: string; server: string; internal: string } | null = null; @@ -59,7 +49,7 @@ export function astroEnv({ const validatedVariables = validatePublicVariables({ schema, loadedEnv, - validateSecrets: settings.config.experimental.env?.validateSecrets ?? false, + validateSecrets, }); templates = { diff --git a/packages/astro/templates/env/module.mjs b/packages/astro/templates/env.mjs similarity index 100% rename from packages/astro/templates/env/module.mjs rename to packages/astro/templates/env.mjs diff --git a/packages/astro/templates/env/types.d.ts b/packages/astro/types/env.d.ts similarity index 59% rename from packages/astro/templates/env/types.d.ts rename to packages/astro/types/env.d.ts index 5af1ac6a10..6dc51a2213 100644 --- a/packages/astro/templates/env/types.d.ts +++ b/packages/astro/types/env.d.ts @@ -1,9 +1,5 @@ -declare module 'astro:env/client' { - // @@CLIENT@@ -} +declare module 'astro:env/client' {} declare module 'astro:env/server' { - // @@SERVER@@ - export const getSecret: (key: string) => string | undefined; }