Skip to content

Commit 94ac7ef

Browse files
florian-lefebvresarah11918
andauthoredJun 11, 2024··
feat(astro): address astro env rfc feedback (#11213)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
1 parent 02e5617 commit 94ac7ef

File tree

16 files changed

+93
-135
lines changed

16 files changed

+93
-135
lines changed
 

‎.changeset/clever-jars-trade.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Removes the `PUBLIC_` prefix constraint for `astro:env` public variables

‎.changeset/dirty-rabbits-act.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
**BREAKING CHANGE to the experimental `astro:env` feature only**
6+
7+
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:
8+
9+
```diff
10+
- import { getSecret } from 'astro:env/server'
11+
- const API_SECRET = getSecret("API_SECRET")
12+
+ import { API_SECRET } from 'astro:env/server'
13+
```
14+
15+
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.

‎packages/astro/src/@types/astro.ts

+14-15
Original file line numberDiff line numberDiff line change
@@ -2070,17 +2070,17 @@ export interface AstroUserConfig {
20702070
*
20712071
* ```astro
20722072
* ---
2073-
* import { PUBLIC_APP_ID } from "astro:env/client"
2074-
* import { PUBLIC_API_URL, getSecret } from "astro:env/server"
2075-
* const API_TOKEN = getSecret("API_TOKEN")
2073+
* import { APP_ID } from "astro:env/client"
2074+
* import { API_URL, API_TOKEN, getSecret } from "astro:env/server"
2075+
* const NODE_ENV = getSecret("NODE_ENV")
20762076
*
2077-
* const data = await fetch(`${PUBLIC_API_URL}/users`, {
2077+
* const data = await fetch(`${API_URL}/users`, {
20782078
* method: "POST",
20792079
* headers: {
20802080
* "Content-Type": "application/json",
20812081
* "Authorization": `Bearer ${API_TOKEN}`
20822082
* },
2083-
* body: JSON.stringify({ appId: PUBLIC_APP_ID })
2083+
* body: JSON.stringify({ appId: APP_ID, nodeEnv: NODE_ENV })
20842084
* })
20852085
* ---
20862086
* ```
@@ -2095,37 +2095,36 @@ export interface AstroUserConfig {
20952095
* experimental: {
20962096
* env: {
20972097
* schema: {
2098-
* PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }),
2099-
* PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }),
2098+
* API_URL: envField.string({ context: "client", access: "public", optional: true }),
2099+
* PORT: envField.number({ context: "server", access: "public", default: 4321 }),
21002100
* API_SECRET: envField.string({ context: "server", access: "secret" }),
21012101
* }
21022102
* }
21032103
* }
21042104
* })
21052105
* ```
21062106
*
2107-
* There are currently three data types supported: strings, numbers and booleans.
2107+
* There are currently four data types supported: strings, numbers, booleans and enums.
21082108
*
21092109
* 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):
21102110
*
21112111
* - **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:
21122112
*
21132113
* ```js
2114-
* import { PUBLIC_API_URL } from "astro:env/client"
2114+
* import { API_URL } from "astro:env/client"
21152115
* ```
21162116
*
21172117
* - **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:
21182118
*
21192119
* ```js
2120-
* import { PUBLIC_PORT } from "astro:env/server"
2120+
* import { PORT } from "astro:env/server"
21212121
* ```
21222122
*
2123-
* - **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:
2123+
* - **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:
21242124
*
21252125
* ```js
2126-
* import { getSecret } from "astro:env/server"
2126+
* import { API_SECRET, getSecret } from "astro:env/server"
21272127
*
2128-
* const API_SECRET = getSecret("API_SECRET") // typed
21292128
* const SECRET_NOT_IN_SCHEMA = getSecret("SECRET_NOT_IN_SCHEMA") // string | undefined
21302129
* ```
21312130
*
@@ -2152,8 +2151,8 @@ export interface AstroUserConfig {
21522151
* experimental: {
21532152
* env: {
21542153
* schema: {
2155-
* PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }),
2156-
* PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }),
2154+
* API_URL: envField.string({ context: "client", access: "public", optional: true }),
2155+
* PORT: envField.number({ context: "server", access: "public", default: 4321 }),
21572156
* API_SECRET: envField.string({ context: "server", access: "secret" }),
21582157
* }
21592158
* }

‎packages/astro/src/core/base-pipeline.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export abstract class Pipeline {
6666
if (callSetGetEnv && manifest.experimentalEnvGetSecretEnabled) {
6767
setGetEnv(() => {
6868
throw new AstroError(AstroErrorData.EnvUnsupportedGetSecret);
69-
});
69+
}, true);
7070
}
7171
}
7272

‎packages/astro/src/env/constants.ts

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export const VIRTUAL_MODULES_IDS = {
55
};
66
export const VIRTUAL_MODULES_IDS_VALUES = new Set(Object.values(VIRTUAL_MODULES_IDS));
77

8-
export const PUBLIC_PREFIX = 'PUBLIC_';
98
export const ENV_TYPES_FILE = 'env.d.ts';
109

1110
const PKG_BASE = new URL('../../', import.meta.url);

‎packages/astro/src/env/runtime.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ export type GetEnv = (key: string) => string | undefined;
55

66
let _getEnv: GetEnv = (key) => process.env[key];
77

8-
export function setGetEnv(fn: GetEnv) {
8+
export function setGetEnv(fn: GetEnv, reset = false) {
99
_getEnv = fn;
10+
11+
_onSetGetEnv(reset);
12+
}
13+
14+
let _onSetGetEnv = (reset: boolean) => {};
15+
16+
export function setOnSetGetEnv(fn: typeof _onSetGetEnv) {
17+
_onSetGetEnv = fn;
1018
}
1119

1220
export function getEnv(...args: Parameters<GetEnv>) {

‎packages/astro/src/env/schema.ts

+6-24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { z } from 'zod';
2-
import { PUBLIC_PREFIX } from './constants.js';
32

43
const StringSchema = z.object({
54
type: z.literal('string'),
@@ -84,29 +83,12 @@ const EnvFieldMetadata = z.union([
8483

8584
const KEY_REGEX = /^[A-Z_]+$/;
8685

87-
export const EnvSchema = z
88-
.record(
89-
z.string().regex(KEY_REGEX, {
90-
message: 'A valid variable name can only contain uppercase letters and underscores.',
91-
}),
92-
z.intersection(EnvFieldMetadata, EnvFieldType)
93-
)
94-
.superRefine((schema, ctx) => {
95-
for (const [key, value] of Object.entries(schema)) {
96-
if (key.startsWith(PUBLIC_PREFIX) && value.access !== 'public') {
97-
ctx.addIssue({
98-
code: z.ZodIssueCode.custom,
99-
message: `An environment variable whose name is prefixed by "${PUBLIC_PREFIX}" must be public.`,
100-
});
101-
}
102-
if (value.access === 'public' && !key.startsWith(PUBLIC_PREFIX)) {
103-
ctx.addIssue({
104-
code: z.ZodIssueCode.custom,
105-
message: `An environment variable that is public must have a name prefixed by "${PUBLIC_PREFIX}".`,
106-
});
107-
}
108-
}
109-
});
86+
export const EnvSchema = z.record(
87+
z.string().regex(KEY_REGEX, {
88+
message: 'A valid variable name can only contain uppercase letters and underscores.',
89+
}),
90+
z.intersection(EnvFieldMetadata, EnvFieldType)
91+
);
11092

11193
// https://www.totaltypescript.com/concepts/the-prettify-helper
11294
type Prettify<T> = {

‎packages/astro/src/env/vite-plugin-env.ts

+16-21
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,8 @@ export function astroEnv({
6767
fs,
6868
content: getDts({
6969
fs,
70-
clientPublic: clientTemplates.types,
71-
serverPublic: serverTemplates.types.public,
72-
serverSecret: serverTemplates.types.secret,
70+
client: clientTemplates.types,
71+
server: serverTemplates.types,
7372
}),
7473
});
7574
},
@@ -154,22 +153,17 @@ function validatePublicVariables({
154153
}
155154

156155
function getDts({
157-
clientPublic,
158-
serverPublic,
159-
serverSecret,
156+
client,
157+
server,
160158
fs,
161159
}: {
162-
clientPublic: string;
163-
serverPublic: string;
164-
serverSecret: string;
160+
client: string;
161+
server: string;
165162
fs: typeof fsMod;
166163
}) {
167164
const template = fs.readFileSync(TYPES_TEMPLATE_URL, 'utf-8');
168165

169-
return template
170-
.replace('// @@CLIENT@@', clientPublic)
171-
.replace('// @@SERVER@@', serverPublic)
172-
.replace('// @@SECRET_VALUES@@', serverSecret);
166+
return template.replace('// @@CLIENT@@', client).replace('// @@SERVER@@', server);
173167
}
174168

175169
function getClientTemplates({
@@ -201,27 +195,28 @@ function getServerTemplates({
201195
fs: typeof fsMod;
202196
}) {
203197
let module = fs.readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
204-
let publicTypes = '';
205-
let secretTypes = '';
198+
let types = '';
199+
let onSetGetEnv = '';
206200

207201
for (const { key, type, value } of validatedVariables.filter((e) => e.context === 'server')) {
208202
module += `export const ${key} = ${JSON.stringify(value)};`;
209-
publicTypes += `export const ${key}: ${type}; \n`;
203+
types += `export const ${key}: ${type}; \n`;
210204
}
211205

212206
for (const [key, options] of Object.entries(schema)) {
213207
if (!(options.context === 'server' && options.access === 'secret')) {
214208
continue;
215209
}
216210

217-
secretTypes += `${key}: ${getEnvFieldType(options)}; \n`;
211+
types += `export const ${key}: ${getEnvFieldType(options)}; \n`;
212+
module += `export let ${key} = _internalGetSecret(${JSON.stringify(key)});\n`;
213+
onSetGetEnv += `${key} = reset ? undefined : _internalGetSecret(${JSON.stringify(key)});\n`;
218214
}
219215

216+
module = module.replace('// @@ON_SET_GET_ENV@@', onSetGetEnv);
217+
220218
return {
221219
module,
222-
types: {
223-
public: publicTypes,
224-
secret: secretTypes,
225-
},
220+
types,
226221
};
227222
}
+14-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
import { schema } from 'virtual:astro:env/internal';
2-
import { createInvalidVariableError, getEnv, validateEnvVariable } from 'astro/env/runtime';
2+
import {
3+
createInvalidVariableError,
4+
getEnv,
5+
validateEnvVariable,
6+
setOnSetGetEnv,
7+
} from 'astro/env/runtime';
38

49
export const getSecret = (key) => {
10+
return getEnv(key);
11+
};
12+
13+
const _internalGetSecret = (key) => {
514
const rawVariable = getEnv(key);
615
const variable = rawVariable === '' ? undefined : rawVariable;
716
const options = schema[key];
817

9-
if (!options) {
10-
return variable;
11-
}
12-
1318
const result = validateEnvVariable(variable, options);
1419
if (result.ok) {
1520
return result.value;
1621
}
1722
throw createInvalidVariableError(key, result.type);
1823
};
24+
25+
setOnSetGetEnv((reset) => {
26+
// @@ON_SET_GET_ENV@@
27+
});

‎packages/astro/templates/env/types.d.ts

+1-12
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,5 @@ declare module 'astro:env/client' {
55
declare module 'astro:env/server' {
66
// @@SERVER@@
77

8-
type SecretValues = {
9-
// @@SECRET_VALUES@@
10-
};
11-
12-
type SecretValue = keyof SecretValues;
13-
14-
type Loose<T> = T | (string & {});
15-
type Strictify<T extends string> = T extends `${infer _}` ? T : never;
16-
17-
export const getSecret: <TKey extends Loose<SecretValue>>(
18-
key: TKey
19-
) => TKey extends Strictify<SecretValue> ? SecretValues[TKey] : string | undefined;
8+
export const getSecret: (key: string) => string | undefined;
209
}

‎packages/astro/test/fixtures/astro-env-server-fail/astro.config.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default defineConfig({
55
experimental: {
66
env: {
77
schema: {
8-
PUBLIC_FOO: envField.string({ context: "server", access: "public", optional: true, default: "ABC" }),
8+
FOO: envField.string({ context: "server", access: "public", optional: true, default: "ABC" }),
99
}
1010
}
1111
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<script>
2-
import { PUBLIC_FOO } from "astro:env/server"
2+
import { FOO } from "astro:env/server"
33
</script>

‎packages/astro/test/fixtures/astro-env-server-secret/src/pages/index.astro

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
2-
import { getSecret } from "astro:env/server"
2+
import { getSecret, KNOWN_SECRET } from "astro:env/server"
33
4-
const KNOWN_SECRET = getSecret("KNOWN_SECRET")
54
const UNKNOWN_SECRET = getSecret("UNKNOWN_SECRET")
65
---
76

‎packages/astro/test/fixtures/astro-env/astro.config.mjs

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ export default defineConfig({
55
experimental: {
66
env: {
77
schema: {
8-
PUBLIC_FOO: envField.string({ context: "client", access: "public", optional: true, default: "ABC" }),
9-
PUBLIC_BAR: envField.string({ context: "client", access: "public", optional: true, default: "DEF" }),
10-
PUBLIC_BAZ: envField.string({ context: "server", access: "public", optional: true, default: "GHI" }),
8+
FOO: envField.string({ context: "client", access: "public", optional: true, default: "ABC" }),
9+
BAR: envField.string({ context: "client", access: "public", optional: true, default: "DEF" }),
10+
BAZ: envField.string({ context: "server", access: "public", optional: true, default: "GHI" }),
1111
}
1212
}
1313
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
---
2-
import { PUBLIC_FOO } from "astro:env/client"
3-
import { PUBLIC_BAZ } from "astro:env/server"
2+
import { FOO } from "astro:env/client"
3+
import { BAZ } from "astro:env/server"
44
5-
console.log({ PUBLIC_BAZ })
5+
console.log({ BAZ })
66
---
77

8-
<div id="server-rendered">{PUBLIC_FOO}</div>
8+
<div id="server-rendered">{FOO}</div>
99
<div id="client-rendered"></div>
1010

1111
<script>
12-
import { PUBLIC_BAR } from "astro:env/client"
12+
import { BAR } from "astro:env/client"
1313

14-
document.getElementById("client-rendered").innerText = PUBLIC_BAR
14+
document.getElementById("client-rendered").innerText = BAR
1515
</script>

‎packages/astro/test/units/config/config-validate.test.js

-42
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import stripAnsi from 'strip-ansi';
44
import { z } from 'zod';
55
import { validateConfig } from '../../../dist/core/config/config.js';
66
import { formatConfigErrorMessage } from '../../../dist/core/messages.js';
7-
import { envField } from '../../../dist/env/config.js';
87

98
describe('Config Validation', () => {
109
it('empty user config is valid', async () => {
@@ -368,46 +367,5 @@ describe('Config Validation', () => {
368367
).catch((err) => err)
369368
);
370369
});
371-
372-
it('Should not allow client variables without a PUBLIC_ prefix', async () => {
373-
const configError = await validateConfig(
374-
{
375-
experimental: {
376-
env: {
377-
schema: {
378-
FOO: envField.string({ context: 'client', access: 'public' }),
379-
},
380-
},
381-
},
382-
},
383-
process.cwd()
384-
).catch((err) => err);
385-
assert.equal(configError instanceof z.ZodError, true);
386-
assert.equal(
387-
configError.errors[0].message,
388-
'An environment variable that is public must have a name prefixed by "PUBLIC_".'
389-
);
390-
});
391-
392-
it('Should not allow non client variables with a PUBLIC_ prefix', async () => {
393-
const configError = await validateConfig(
394-
{
395-
experimental: {
396-
env: {
397-
schema: {
398-
FOO: envField.string({ context: 'server', access: 'public' }),
399-
},
400-
},
401-
},
402-
},
403-
process.cwd()
404-
).catch((err) => err);
405-
assert.equal(configError instanceof z.ZodError, true);
406-
console.log(configError);
407-
assert.equal(
408-
configError.errors[0].message,
409-
'An environment variable that is public must have a name prefixed by "PUBLIC_".'
410-
);
411-
});
412370
});
413371
});

0 commit comments

Comments
 (0)
Please sign in to comment.