Skip to content

Commit

Permalink
feat(astro): experimental astro:env (#10974)
Browse files Browse the repository at this point in the history
* feat(env): add schema, types and envField (#10805)

* feat(env): add validators (#10827)

* feat(env): add vite plugin (#10829)

* feat(env): client/public variables (#10848)

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* feat(env): server/public variables (#10881)

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* feat(env): server/secret variables (#10954)

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* fix: import

* fix: test

* feat: work on jsdoc

* feat: more jsdoc

* chore: remove todo

* feat: fix test error and write changeset

* feat: update config reference

* feat: apply recommendations from review

* feat: rework getEnv/setGetEnv

* chore: move tests

* fix: rename

* fix: dev mode

* chore: improve error

* feat: add overrideProcessEnv helper

* fix: make eslint happy

* Update .changeset/poor-berries-occur.md

Co-authored-by: Paul Valladares <85648028+dreyfus92@users.noreply.github.com>

* Update .changeset/poor-berries-occur.md

Co-authored-by: Paul Valladares <85648028+dreyfus92@users.noreply.github.com>

* Update .changeset/poor-berries-occur.md

Co-authored-by: Paul Valladares <85648028+dreyfus92@users.noreply.github.com>

* Update .changeset/poor-berries-occur.md

Co-authored-by: Paul Valladares <85648028+dreyfus92@users.noreply.github.com>

* feat: fix build and address reviews

* fix: container api

* fix: tests

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update packages/astro/src/@types/astro.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update packages/astro/src/@types/astro.ts

* chore: update changeset

* feat: address reviews

* feat: address Ema's reviews

* Update .changeset/poor-berries-occur.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

---------

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
Co-authored-by: Paul Valladares <85648028+dreyfus92@users.noreply.github.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
5 people authored Jun 5, 2024
1 parent 803dd80 commit 2668ef9
Show file tree
Hide file tree
Showing 57 changed files with 1,594 additions and 184 deletions.
70 changes: 70 additions & 0 deletions .changeset/poor-berries-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
"astro": minor
---

Adds experimental support for the `astro:env` API.

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 { PUBLIC_APP_ID } from "astro:env/client"
import { PUBLIC_API_URL, getSecret } from "astro:env/server"
const API_TOKEN = getSecret("API_TOKEN")
const data = await fetch(`${PUBLIC_API_URL}/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_TOKEN}`
},
body: JSON.stringify({ appId: PUBLIC_APP_ID })
})
---
```

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: {
PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }),
PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }),
API_SECRET: envField.string({ context: "server", access: "secret" }),
}
}
}
})
```

There are three kinds of environment variables, determined by the combination of `context` (`client` or `server`) and `access` (`private` 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"
```

- **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"
```

- **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:

```js
import { getSecret } from "astro:env/server"

const API_SECRET = getSecret("API_SECRET") // typed
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.

To learn more, check out [the documentation](https://docs.astro.build/en/reference/configuration-reference/#experimentalenv).
4 changes: 4 additions & 0 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ declare module 'astro:components' {
export * from 'astro/components';
}

declare module 'astro:env/setup' {
export * from 'astro/virtual-modules/env-setup.js'
}

type MD = import('./dist/@types/astro.js').MarkdownInstance<Record<string, any>>;
interface ExportedMarkdownModuleEntities {
frontmatter: MD['frontmatter'];
Expand Down
6 changes: 6 additions & 0 deletions packages/astro/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type AstroUserConfig = import('./dist/@types/astro.js').AstroUserConfig;
type AstroInlineConfig = import('./dist/@types/astro.js').AstroInlineConfig;
type ImageServiceConfig = import('./dist/@types/astro.js').ImageServiceConfig;
type SharpImageServiceConfig = import('./dist/assets/services/sharp.js').SharpImageServiceConfig;
type EnvField = typeof import('./dist/env/config.js').envField;

/**
* See the full Astro Configuration API Documentation
Expand Down Expand Up @@ -37,3 +38,8 @@ export function squooshImageService(): ImageServiceConfig;
* See: https://docs.astro.build/en/guides/images/#configure-no-op-passthrough-service
*/
export function passthroughImageService(): ImageServiceConfig;

/**
* Return a valid env field to use in this Astro config for `experimental.env.schema`.
*/
export const envField: EnvField;
1 change: 1 addition & 0 deletions packages/astro/config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { defineConfig, getViteConfig } from './dist/config/index.js';
export { envField } from './dist/env/config.js';

export function sharpImageService(config = {}) {
return {
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"default": "./dist/core/index.js"
},
"./env": "./env.d.ts",
"./env/runtime": "./dist/env/runtime.js",
"./types": "./types.d.ts",
"./client": "./client.d.ts",
"./astro-jsx": "./astro-jsx.d.ts",
Expand Down Expand Up @@ -100,8 +101,6 @@
"env.d.ts",
"client.d.ts",
"jsx-runtime.d.ts",
"content-types.template.d.ts",
"content-module.template.mjs",
"templates",
"astro-jsx.d.ts",
"types.d.ts",
Expand Down
114 changes: 114 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import type {
} from '../transitions/events.js';
import type { DeepPartial, OmitIndexSignature, Simplify, WithRequired } from '../type-utils.js';
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
import type { EnvSchema } from '../env/schema.js';

export type { AstroIntegrationLogger, ToolbarServerHelpers };

Expand Down Expand Up @@ -2054,6 +2055,113 @@ export interface AstroUserConfig {
* For a complete overview, and to give feedback on this experimental API, see the [Rerouting RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md).
*/
rewriting?: 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 { PUBLIC_APP_ID } from "astro:env/client"
* import { PUBLIC_API_URL, getSecret } from "astro:env/server"
* const API_TOKEN = getSecret("API_TOKEN")
*
* const data = await fetch(`${PUBLIC_API_URL}/users`, {
* method: "POST",
* headers: {
* "Content-Type": "application/json",
* "Authorization": `Bearer ${API_TOKEN}`
* },
* body: JSON.stringify({ appId: PUBLIC_APP_ID })
* })
* ---
* ```
*
* 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: {
* PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }),
* PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }),
* API_SECRET: envField.string({ context: "server", access: "secret" }),
* }
* }
* }
* })
* ```
*
* There are currently 3 data types supported: strings, numbers and booleans.
*
* There are three kinds of variables, determined by the combination of `context` (`client` or `server`) and `access` (`private` 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"
* ```
*
* - **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"
* ```
*
* - **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:
*
* ```js
* import { getSecret } from "astro:env/server"
*
* const API_SECRET = getSecret("API_SECRET") // typed
* 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
* @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: {
* PUBLIC_API_URL: envField.string({ context: "client", access: "public", optional: true }),
* PUBLIC_PORT: envField.number({ context: "server", access: "public", default: 4321 }),
* API_SECRET: envField.string({ context: "server", access: "secret" }),
* }
* }
* }
* })
* ```
*/
schema?: EnvSchema;
};
};
}

Expand Down Expand Up @@ -2243,6 +2351,7 @@ export interface AstroSettings {
tsConfigPath: string | undefined;
watchFiles: string[];
timer: AstroTimer;
dotAstroDir: URL;
/**
* Latest version of Astro, will be undefined if:
* - unable to check
Expand Down Expand Up @@ -2547,6 +2656,11 @@ export type AstroFeatureMap = {
* List of features that orbit around the i18n routing
*/
i18nDomains?: SupportsKind;

/**
* The adapter is able to support `getSecret` exported from `astro:env/server`
*/
envGetSecret?: SupportsKind;
};

export interface AstroAssetsFeature {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ function createManifest(
i18n: manifest?.i18n,
checkOrigin: false,
middleware: manifest?.middleware ?? middleware ?? defaultMiddleware,
experimentalEnvGetSecretEnabled: false,
};
}

Expand Down
10 changes: 5 additions & 5 deletions packages/astro/src/content/types-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ async function writeContentFiles({
let contentTypesStr = '';
let dataTypesStr = '';

const collectionSchemasDir = new URL('./collections/', contentPaths.cacheDir);
const collectionSchemasDir = new URL('./collections/', settings.dotAstroDir);
if (
settings.config.experimental.contentCollectionJsonSchema &&
!fs.existsSync(collectionSchemasDir)
Expand Down Expand Up @@ -490,12 +490,12 @@ async function writeContentFiles({
}
}

if (!fs.existsSync(contentPaths.cacheDir)) {
fs.mkdirSync(contentPaths.cacheDir, { recursive: true });
if (!fs.existsSync(settings.dotAstroDir)) {
fs.mkdirSync(settings.dotAstroDir, { recursive: true });
}

const configPathRelativeToCacheDir = normalizeConfigPath(
contentPaths.cacheDir.pathname,
settings.dotAstroDir.pathname,
contentPaths.config.url.pathname
);

Expand All @@ -512,7 +512,7 @@ async function writeContentFiles({
);

await fs.promises.writeFile(
new URL(CONTENT_TYPES_FILE, contentPaths.cacheDir),
new URL(CONTENT_TYPES_FILE, settings.dotAstroDir),
typeTemplateContent
);
}
10 changes: 4 additions & 6 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { createImage } from './runtime-assets.js';
/**
* Amap from a collection + slug to the local file path.
* This is used internally to resolve entry imports when using `getEntry()`.
* @see `content-module.template.mjs`
* @see `templates/content/module.mjs`
*/
export type ContentLookupMap = {
[collectionName: string]: { type: 'content' | 'data'; entries: { [lookupId: string]: string } };
Expand Down Expand Up @@ -419,7 +419,6 @@ export function contentObservable(initialCtx: ContentCtx): ContentObservable {
export type ContentPaths = {
contentDir: URL;
assetsDir: URL;
cacheDir: URL;
typesTemplate: URL;
virtualModTemplate: URL;
config: {
Expand All @@ -429,17 +428,16 @@ export type ContentPaths = {
};

export function getContentPaths(
{ srcDir, root }: Pick<AstroConfig, 'root' | 'srcDir'>,
{ srcDir }: Pick<AstroConfig, 'root' | 'srcDir'>,
fs: typeof fsMod = fsMod
): ContentPaths {
const configStats = search(fs, srcDir);
const pkgBase = new URL('../../', import.meta.url);
return {
cacheDir: new URL('.astro/', root),
contentDir: new URL('./content/', srcDir),
assetsDir: new URL('./assets/', srcDir),
typesTemplate: new URL('content-types.template.d.ts', pkgBase),
virtualModTemplate: new URL('content-module.template.mjs', pkgBase),
typesTemplate: new URL('templates/content/types.d.ts', pkgBase),
virtualModTemplate: new URL('templates/content/module.mjs', pkgBase),
config: configStats,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ function getStringifiedCollectionFromLookup(
/**
* Generate a map from a collection + slug to the local file path.
* This is used internally to resolve entry imports when using `getEntry()`.
* @see `content-module.template.mjs`
* @see `templates/content/module.mjs`
*/
export async function generateLookupMap({
settings,
Expand Down
11 changes: 10 additions & 1 deletion packages/astro/src/core/app/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,16 @@ export class AppPipeline extends Pipeline {
renderers,
resolve,
serverLike,
streaming
streaming,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
false,
);
pipeline.#manifestData = manifestData;
return pipeline;
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export type SSRManifest = {
checkOrigin: boolean;
// TODO: remove once the experimental flag is removed
rewritingEnabled: boolean;
// TODO: remove experimental prefix
experimentalEnvGetSecretEnabled: boolean;
};

export type SSRManifestI18n = {
Expand Down
Loading

0 comments on commit 2668ef9

Please sign in to comment.