diff --git a/.changeset/clever-jars-trade.md b/.changeset/clever-jars-trade.md new file mode 100644 index 0000000000..8a632d3f96 --- /dev/null +++ b/.changeset/clever-jars-trade.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Removes the `PUBLIC_` prefix constraint for `astro:env` public variables diff --git a/.changeset/dirty-rabbits-act.md b/.changeset/dirty-rabbits-act.md new file mode 100644 index 0000000000..65c8ab542d --- /dev/null +++ b/.changeset/dirty-rabbits-act.md @@ -0,0 +1,15 @@ +--- +'astro': patch +--- + +**BREAKING CHANGE to the experimental `astro:env` feature only** + +Server secrets specified in the schema must now be imported from `astro:env/server`. Using `getSecret()` is no longer required to use these environment variables in your schema: + +```diff +- import { getSecret } from 'astro:env/server' +- const API_SECRET = getSecret("API_SECRET") ++ import { API_SECRET } from 'astro:env/server' +``` + +Note that using `getSecret()` with these keys is still possible, but no longer involves any special handling and the raw value will be returned, just like retrieving secrets not specified in your schema. diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 70e09d6c43..d30b1b3bab 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -2070,17 +2070,17 @@ export interface AstroUserConfig { * * ```astro * --- - * import { PUBLIC_APP_ID } from "astro:env/client" - * import { PUBLIC_API_URL, getSecret } from "astro:env/server" - * const API_TOKEN = getSecret("API_TOKEN") + * import { APP_ID } from "astro:env/client" + * import { API_URL, API_TOKEN, getSecret } from "astro:env/server" + * const NODE_ENV = getSecret("NODE_ENV") * - * const data = await fetch(`${PUBLIC_API_URL}/users`, { + * const data = await fetch(`${API_URL}/users`, { * method: "POST", * headers: { * "Content-Type": "application/json", * "Authorization": `Bearer ${API_TOKEN}` * }, - * body: JSON.stringify({ appId: PUBLIC_APP_ID }) + * body: JSON.stringify({ appId: APP_ID, nodeEnv: NODE_ENV }) * }) * --- * ``` @@ -2095,8 +2095,8 @@ export interface AstroUserConfig { * experimental: { * env: { * schema: { - * PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }), - * PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }), + * 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" }), * } * } @@ -2104,28 +2104,27 @@ export interface AstroUserConfig { * }) * ``` * - * There are currently three data types supported: strings, numbers and booleans. + * 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 { PUBLIC_API_URL } from "astro:env/client" + * 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 { PUBLIC_PORT } from "astro:env/server" + * 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 `getSecret()` helper function available from the `astro:env/server` module: + * - **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: * * ```js - * import { getSecret } from "astro:env/server" + * import { API_SECRET, getSecret } from "astro:env/server" * - * const API_SECRET = getSecret("API_SECRET") // typed * const SECRET_NOT_IN_SCHEMA = getSecret("SECRET_NOT_IN_SCHEMA") // string | undefined * ``` * @@ -2152,8 +2151,8 @@ export interface AstroUserConfig { * experimental: { * env: { * schema: { - * PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }), - * PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }), + * 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" }), * } * } diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index b91bb6202f..b9eccc327c 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -66,7 +66,7 @@ export abstract class Pipeline { if (callSetGetEnv && manifest.experimentalEnvGetSecretEnabled) { setGetEnv(() => { throw new AstroError(AstroErrorData.EnvUnsupportedGetSecret); - }); + }, true); } } diff --git a/packages/astro/src/env/constants.ts b/packages/astro/src/env/constants.ts index 19ea17c64f..de5f06233d 100644 --- a/packages/astro/src/env/constants.ts +++ b/packages/astro/src/env/constants.ts @@ -5,7 +5,6 @@ export const VIRTUAL_MODULES_IDS = { }; export const VIRTUAL_MODULES_IDS_VALUES = new Set(Object.values(VIRTUAL_MODULES_IDS)); -export const PUBLIC_PREFIX = 'PUBLIC_'; export const ENV_TYPES_FILE = 'env.d.ts'; const PKG_BASE = new URL('../../', import.meta.url); diff --git a/packages/astro/src/env/runtime.ts b/packages/astro/src/env/runtime.ts index 9336a671be..317e9110fe 100644 --- a/packages/astro/src/env/runtime.ts +++ b/packages/astro/src/env/runtime.ts @@ -5,8 +5,16 @@ export type GetEnv = (key: string) => string | undefined; let _getEnv: GetEnv = (key) => process.env[key]; -export function setGetEnv(fn: GetEnv) { +export function setGetEnv(fn: GetEnv, reset = false) { _getEnv = fn; + + _onSetGetEnv(reset); +} + +let _onSetGetEnv = (reset: boolean) => {}; + +export function setOnSetGetEnv(fn: typeof _onSetGetEnv) { + _onSetGetEnv = fn; } export function getEnv(...args: Parameters) { diff --git a/packages/astro/src/env/schema.ts b/packages/astro/src/env/schema.ts index fd41998585..ec2e128279 100644 --- a/packages/astro/src/env/schema.ts +++ b/packages/astro/src/env/schema.ts @@ -1,5 +1,4 @@ import { z } from 'zod'; -import { PUBLIC_PREFIX } from './constants.js'; const StringSchema = z.object({ type: z.literal('string'), @@ -84,29 +83,12 @@ const EnvFieldMetadata = z.union([ const KEY_REGEX = /^[A-Z_]+$/; -export const EnvSchema = z - .record( - z.string().regex(KEY_REGEX, { - message: 'A valid variable name can only contain uppercase letters and underscores.', - }), - z.intersection(EnvFieldMetadata, EnvFieldType) - ) - .superRefine((schema, ctx) => { - for (const [key, value] of Object.entries(schema)) { - if (key.startsWith(PUBLIC_PREFIX) && value.access !== 'public') { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `An environment variable whose name is prefixed by "${PUBLIC_PREFIX}" must be public.`, - }); - } - if (value.access === 'public' && !key.startsWith(PUBLIC_PREFIX)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `An environment variable that is public must have a name prefixed by "${PUBLIC_PREFIX}".`, - }); - } - } - }); +export const EnvSchema = z.record( + z.string().regex(KEY_REGEX, { + message: 'A valid variable name can only contain uppercase letters and underscores.', + }), + z.intersection(EnvFieldMetadata, EnvFieldType) +); // https://www.totaltypescript.com/concepts/the-prettify-helper type Prettify = { diff --git a/packages/astro/src/env/vite-plugin-env.ts b/packages/astro/src/env/vite-plugin-env.ts index 2f6a0708f1..7ca5e4b0a0 100644 --- a/packages/astro/src/env/vite-plugin-env.ts +++ b/packages/astro/src/env/vite-plugin-env.ts @@ -67,9 +67,8 @@ export function astroEnv({ fs, content: getDts({ fs, - clientPublic: clientTemplates.types, - serverPublic: serverTemplates.types.public, - serverSecret: serverTemplates.types.secret, + client: clientTemplates.types, + server: serverTemplates.types, }), }); }, @@ -154,22 +153,17 @@ function validatePublicVariables({ } function getDts({ - clientPublic, - serverPublic, - serverSecret, + client, + server, fs, }: { - clientPublic: string; - serverPublic: string; - serverSecret: string; + client: string; + server: string; fs: typeof fsMod; }) { const template = fs.readFileSync(TYPES_TEMPLATE_URL, 'utf-8'); - return template - .replace('// @@CLIENT@@', clientPublic) - .replace('// @@SERVER@@', serverPublic) - .replace('// @@SECRET_VALUES@@', serverSecret); + return template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server); } function getClientTemplates({ @@ -201,12 +195,12 @@ function getServerTemplates({ fs: typeof fsMod; }) { let module = fs.readFileSync(MODULE_TEMPLATE_URL, 'utf-8'); - let publicTypes = ''; - let secretTypes = ''; + let types = ''; + let onSetGetEnv = ''; for (const { key, type, value } of validatedVariables.filter((e) => e.context === 'server')) { module += `export const ${key} = ${JSON.stringify(value)};`; - publicTypes += `export const ${key}: ${type}; \n`; + types += `export const ${key}: ${type}; \n`; } for (const [key, options] of Object.entries(schema)) { @@ -214,14 +208,15 @@ function getServerTemplates({ continue; } - secretTypes += `${key}: ${getEnvFieldType(options)}; \n`; + types += `export const ${key}: ${getEnvFieldType(options)}; \n`; + module += `export let ${key} = _internalGetSecret(${JSON.stringify(key)});\n`; + onSetGetEnv += `${key} = reset ? undefined : _internalGetSecret(${JSON.stringify(key)});\n`; } + module = module.replace('// @@ON_SET_GET_ENV@@', onSetGetEnv); + return { module, - types: { - public: publicTypes, - secret: secretTypes, - }, + types, }; } diff --git a/packages/astro/templates/env/module.mjs b/packages/astro/templates/env/module.mjs index 602cb8527a..c88c2caec8 100644 --- a/packages/astro/templates/env/module.mjs +++ b/packages/astro/templates/env/module.mjs @@ -1,18 +1,27 @@ import { schema } from 'virtual:astro:env/internal'; -import { createInvalidVariableError, getEnv, validateEnvVariable } from 'astro/env/runtime'; +import { + createInvalidVariableError, + getEnv, + validateEnvVariable, + setOnSetGetEnv, +} from 'astro/env/runtime'; export const getSecret = (key) => { + return getEnv(key); +}; + +const _internalGetSecret = (key) => { const rawVariable = getEnv(key); const variable = rawVariable === '' ? undefined : rawVariable; const options = schema[key]; - if (!options) { - return variable; - } - const result = validateEnvVariable(variable, options); if (result.ok) { return result.value; } throw createInvalidVariableError(key, result.type); }; + +setOnSetGetEnv((reset) => { + // @@ON_SET_GET_ENV@@ +}); diff --git a/packages/astro/templates/env/types.d.ts b/packages/astro/templates/env/types.d.ts index 4fc2c3f40c..5af1ac6a10 100644 --- a/packages/astro/templates/env/types.d.ts +++ b/packages/astro/templates/env/types.d.ts @@ -5,16 +5,5 @@ declare module 'astro:env/client' { declare module 'astro:env/server' { // @@SERVER@@ - type SecretValues = { - // @@SECRET_VALUES@@ - }; - - type SecretValue = keyof SecretValues; - - type Loose = T | (string & {}); - type Strictify = T extends `${infer _}` ? T : never; - - export const getSecret: >( - key: TKey - ) => TKey extends Strictify ? SecretValues[TKey] : string | undefined; + export const getSecret: (key: string) => string | undefined; } diff --git a/packages/astro/test/fixtures/astro-env-server-fail/astro.config.mjs b/packages/astro/test/fixtures/astro-env-server-fail/astro.config.mjs index b3a8a98202..a5b69ee5fc 100644 --- a/packages/astro/test/fixtures/astro-env-server-fail/astro.config.mjs +++ b/packages/astro/test/fixtures/astro-env-server-fail/astro.config.mjs @@ -5,7 +5,7 @@ export default defineConfig({ experimental: { env: { schema: { - PUBLIC_FOO: envField.string({ context: "server", access: "public", optional: true, default: "ABC" }), + FOO: envField.string({ context: "server", access: "public", optional: true, default: "ABC" }), } } } diff --git a/packages/astro/test/fixtures/astro-env-server-fail/src/pages/index.astro b/packages/astro/test/fixtures/astro-env-server-fail/src/pages/index.astro index be3d43c366..e5753dbb01 100644 --- a/packages/astro/test/fixtures/astro-env-server-fail/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-env-server-fail/src/pages/index.astro @@ -1,3 +1,3 @@ diff --git a/packages/astro/test/fixtures/astro-env-server-secret/src/pages/index.astro b/packages/astro/test/fixtures/astro-env-server-secret/src/pages/index.astro index eaf62cf466..2647a92580 100644 --- a/packages/astro/test/fixtures/astro-env-server-secret/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-env-server-secret/src/pages/index.astro @@ -1,7 +1,6 @@ --- -import { getSecret } from "astro:env/server" +import { getSecret, KNOWN_SECRET } from "astro:env/server" -const KNOWN_SECRET = getSecret("KNOWN_SECRET") const UNKNOWN_SECRET = getSecret("UNKNOWN_SECRET") --- diff --git a/packages/astro/test/fixtures/astro-env/astro.config.mjs b/packages/astro/test/fixtures/astro-env/astro.config.mjs index a7e3c9ca99..6b6276e89b 100644 --- a/packages/astro/test/fixtures/astro-env/astro.config.mjs +++ b/packages/astro/test/fixtures/astro-env/astro.config.mjs @@ -5,9 +5,9 @@ export default defineConfig({ experimental: { env: { schema: { - PUBLIC_FOO: envField.string({ context: "client", access: "public", optional: true, default: "ABC" }), - PUBLIC_BAR: envField.string({ context: "client", access: "public", optional: true, default: "DEF" }), - PUBLIC_BAZ: envField.string({ context: "server", access: "public", optional: true, default: "GHI" }), + FOO: envField.string({ context: "client", access: "public", optional: true, default: "ABC" }), + BAR: envField.string({ context: "client", access: "public", optional: true, default: "DEF" }), + BAZ: envField.string({ context: "server", access: "public", optional: true, default: "GHI" }), } } } diff --git a/packages/astro/test/fixtures/astro-env/src/pages/index.astro b/packages/astro/test/fixtures/astro-env/src/pages/index.astro index 4ea369a1f6..d95bfa205a 100644 --- a/packages/astro/test/fixtures/astro-env/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-env/src/pages/index.astro @@ -1,15 +1,15 @@ --- -import { PUBLIC_FOO } from "astro:env/client" -import { PUBLIC_BAZ } from "astro:env/server" +import { FOO } from "astro:env/client" +import { BAZ } from "astro:env/server" -console.log({ PUBLIC_BAZ }) +console.log({ BAZ }) --- -
{PUBLIC_FOO}
+
{FOO}
diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js index fc1e26856b..21d6841c1d 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.js @@ -4,7 +4,6 @@ import stripAnsi from 'strip-ansi'; import { z } from 'zod'; import { validateConfig } from '../../../dist/core/config/config.js'; import { formatConfigErrorMessage } from '../../../dist/core/messages.js'; -import { envField } from '../../../dist/env/config.js'; describe('Config Validation', () => { it('empty user config is valid', async () => { @@ -368,46 +367,5 @@ describe('Config Validation', () => { ).catch((err) => err) ); }); - - it('Should not allow client variables without a PUBLIC_ prefix', async () => { - const configError = await validateConfig( - { - experimental: { - env: { - schema: { - FOO: envField.string({ context: 'client', access: 'public' }), - }, - }, - }, - }, - process.cwd() - ).catch((err) => err); - assert.equal(configError instanceof z.ZodError, true); - assert.equal( - configError.errors[0].message, - 'An environment variable that is public must have a name prefixed by "PUBLIC_".' - ); - }); - - it('Should not allow non client variables with a PUBLIC_ prefix', async () => { - const configError = await validateConfig( - { - experimental: { - env: { - schema: { - FOO: envField.string({ context: 'server', access: 'public' }), - }, - }, - }, - }, - process.cwd() - ).catch((err) => err); - assert.equal(configError instanceof z.ZodError, true); - console.log(configError); - assert.equal( - configError.errors[0].message, - 'An environment variable that is public must have a name prefixed by "PUBLIC_".' - ); - }); }); });