Skip to content

Commit

Permalink
Merge branch 'main' into content-layer
Browse files Browse the repository at this point in the history
  • Loading branch information
ascorbic authored Jul 12, 2024
2 parents eb28c15 + 88e2b43 commit 456b1c3
Show file tree
Hide file tree
Showing 22 changed files with 430 additions and 112 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-socks-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes a case where Astro's config `experimental.env.schema` keys did not allow numbers. Numbers are still not allowed as the first character to be able to generate valid JavaScript identifiers
18 changes: 18 additions & 0 deletions .changeset/nasty-poems-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'astro': patch
---

Expands the `isInputError()` utility from `astro:actions` to accept errors of any type. This should now allow type narrowing from a try / catch block.

```ts
// example.ts
import { actions, isInputError } from 'astro:actions';

try {
await actions.like(new FormData());
} catch (error) {
if (isInputError(error)) {
console.log(error.fields);
}
}
```
5 changes: 5 additions & 0 deletions .changeset/plenty-socks-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Exposes utility types from `astro:actions` for the `defineAction` handler (`ActionHandler`) and the `ActionError` code (`ActionErrorCode`).
23 changes: 23 additions & 0 deletions .changeset/slow-roses-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'astro': patch
---

Adds a new property `experimental.env.validateSecrets` to allow validating private variables on the server.

By default, this is set to `false` and only public variables are checked on start. If enabled, secrets will also be checked on start (dev/build modes). This is useful for example in some CIs 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
}
}
})
```
29 changes: 29 additions & 0 deletions .changeset/small-vans-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'astro': patch
---

Expose new `ActionReturnType` utility from `astro:actions`. This infers the return type of an action by passing `typeof actions.name` as a type argument. This example defines a `like` action that returns `likes` as an object:

```ts
// actions/index.ts
import { defineAction } from 'astro:actions';

export const server = {
like: defineAction({
handler: () => {
/* ... */
return { likes: 42 }
}
})
}
```

In your client code, you can infer this handler return value with `ActionReturnType`:

```ts
// client.ts
import { actions, ActionReturnType } from 'astro:actions';

type LikesResult = ActionReturnType<typeof actions.like>;
// -> { likes: number }
```
5 changes: 5 additions & 0 deletions .changeset/swift-cows-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro": patch
---

Fixes `astro:actions` autocompletion for the `defineAction` `accept` property
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"test:citgm": "pnpm -r --filter=astro test",
"test:match": "cd packages/astro && pnpm run test:match",
"test:unit": "cd packages/astro && pnpm run test:unit",
"test:types": "cd packages/astro && pnpm run test:types",
"test:unit:match": "cd packages/astro && pnpm run test:unit:match",
"test:smoke": "pnpm test:smoke:example && pnpm test:smoke:docs",
"test:smoke:example": "turbo run build --concurrency=100% --filter=\"@example/*\"",
Expand Down
4 changes: 3 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,13 @@
"build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.{ts,js}\" && pnpm run postbuild",
"dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.{ts,js}\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"",
"test": "pnpm run test:node",
"test": "pnpm run test:node && pnpm run test:types",
"test:match": "pnpm run test:node --match",
"test:e2e": "pnpm test:e2e:chrome && pnpm test:e2e:firefox",
"test:e2e:match": "playwright test -g",
"test:e2e:chrome": "playwright test",
"test:e2e:firefox": "playwright test --config playwright.firefox.config.js",
"test:types": "tsc --project tsconfig.tests.json",
"test:node": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
Expand Down Expand Up @@ -220,6 +221,7 @@
"astro-scripts": "workspace:*",
"cheerio": "1.0.0-rc.12",
"eol": "^0.9.1",
"expect-type": "^0.19.0",
"mdast-util-mdx": "^3.0.0",
"mdast-util-mdx-jsx": "^3.1.2",
"memfs": "^4.9.3",
Expand Down
41 changes: 38 additions & 3 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import type {
import type * as babel from '@babel/core';
import type * as rollup from 'rollup';
import type * as vite from 'vite';
import type { Accept, ActionClient, InputSchema } from '../actions/runtime/virtual/server.js';
import type {
ActionAccept,
ActionClient,
ActionInputSchema,
} from '../actions/runtime/virtual/server.js';
import type { RemotePattern } from '../assets/utils/remotePattern.js';
import type { AssetsPrefix, SerializedSSRManifest } from '../core/app/types.js';
import type { PageBuildData } from '../core/build/types.js';
Expand Down Expand Up @@ -2166,6 +2170,37 @@ export interface AstroUserConfig {
* ```
*/
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;
};
};
}
Expand Down Expand Up @@ -2733,8 +2768,8 @@ interface AstroSharedContext<
* Get action result on the server when using a form POST.
*/
getActionResult: <
TAccept extends Accept,
TInputSchema extends InputSchema<TAccept>,
TAccept extends ActionAccept,
TInputSchema extends ActionInputSchema<TAccept>,
TAction extends ActionClient<unknown, TAccept, TInputSchema>,
>(
action: TAction
Expand Down
11 changes: 11 additions & 0 deletions packages/astro/src/actions/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,14 @@ export async function getAction(
}
return actionLookup;
}

/**
* Used to preserve the input schema type in the error object.
* This allows for type inference on the `fields` property
* when type narrowed to an `ActionInputError`.
*
* Example: Action has an input schema of `{ name: z.string() }`.
* When calling the action and checking `isInputError(result.error)`,
* `result.error.fields` will be typed with the `name` field.
*/
export type ErrorInferenceObject = Record<string, any>;
36 changes: 16 additions & 20 deletions packages/astro/src/actions/runtime/virtual/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { z } from 'zod';
import { type ActionAPIContext, getApiContext as _getApiContext } from '../store.js';
import { type MaybePromise } from '../utils.js';
import {
ActionError,
ActionInputError,
type ErrorInferenceObject,
type SafeResult,
callSafely,
} from './shared.js';
import type { ErrorInferenceObject, MaybePromise } from '../utils.js';
import { ActionError, ActionInputError, type SafeResult, callSafely } from './shared.js';

export * from './shared.js';

Expand All @@ -16,19 +10,21 @@ export { z } from 'zod';
/** @deprecated Access context from the second `handler()` parameter. */
export const getApiContext = _getApiContext;

export type Accept = 'form' | 'json';
export type InputSchema<T extends Accept> = T extends 'form'
export type ActionAccept = 'form' | 'json';
export type ActionInputSchema<T extends ActionAccept | undefined> = T extends 'form'
? z.AnyZodObject | z.ZodType<FormData>
: z.ZodType;

type Handler<TInputSchema, TOutput> = TInputSchema extends z.ZodType
export type ActionHandler<TInputSchema, TOutput> = TInputSchema extends z.ZodType
? (input: z.infer<TInputSchema>, context: ActionAPIContext) => MaybePromise<TOutput>
: (input: any, context: ActionAPIContext) => MaybePromise<TOutput>;

export type ActionReturnType<T extends ActionHandler<any, any>> = Awaited<ReturnType<T>>;

export type ActionClient<
TOutput,
TAccept extends Accept,
TInputSchema extends InputSchema<TAccept> | undefined,
TAccept extends ActionAccept | undefined,
TInputSchema extends ActionInputSchema<TAccept> | undefined,
> = TInputSchema extends z.ZodType
? ((
input: TAccept extends 'form' ? FormData : z.input<TInputSchema>
Expand All @@ -50,8 +46,8 @@ export type ActionClient<

export function defineAction<
TOutput,
TAccept extends Accept = 'json',
TInputSchema extends InputSchema<Accept> | undefined = TAccept extends 'form'
TAccept extends ActionAccept | undefined = undefined,
TInputSchema extends ActionInputSchema<ActionAccept> | undefined = TAccept extends 'form'
? // If `input` is omitted, default to `FormData` for forms and `any` for JSON.
z.ZodType<FormData>
: undefined,
Expand All @@ -62,7 +58,7 @@ export function defineAction<
}: {
input?: TInputSchema;
accept?: TAccept;
handler: Handler<TInputSchema, TOutput>;
handler: ActionHandler<TInputSchema, TOutput>;
}): ActionClient<TOutput, TAccept, TInputSchema> {
const serverHandler =
accept === 'form'
Expand All @@ -77,8 +73,8 @@ export function defineAction<
return serverHandler as ActionClient<TOutput, TAccept, TInputSchema>;
}

function getFormServerHandler<TOutput, TInputSchema extends InputSchema<'form'>>(
handler: Handler<TInputSchema, TOutput>,
function getFormServerHandler<TOutput, TInputSchema extends ActionInputSchema<'form'>>(
handler: ActionHandler<TInputSchema, TOutput>,
inputSchema?: TInputSchema
) {
return async (unparsedInput: unknown): Promise<Awaited<TOutput>> => {
Expand All @@ -99,8 +95,8 @@ function getFormServerHandler<TOutput, TInputSchema extends InputSchema<'form'>>
};
}

function getJsonServerHandler<TOutput, TInputSchema extends InputSchema<'json'>>(
handler: Handler<TInputSchema, TOutput>,
function getJsonServerHandler<TOutput, TInputSchema extends ActionInputSchema<'json'>>(
handler: ActionHandler<TInputSchema, TOutput>,
inputSchema?: TInputSchema
) {
return async (unparsedInput: unknown): Promise<Awaited<TOutput>> => {
Expand Down
41 changes: 23 additions & 18 deletions packages/astro/src/actions/runtime/virtual/shared.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import type { z } from 'zod';
import type { MaybePromise } from '../utils.js';

type ActionErrorCode =
| 'BAD_REQUEST'
| 'UNAUTHORIZED'
| 'FORBIDDEN'
| 'NOT_FOUND'
| 'TIMEOUT'
| 'CONFLICT'
| 'PRECONDITION_FAILED'
| 'PAYLOAD_TOO_LARGE'
| 'UNSUPPORTED_MEDIA_TYPE'
| 'UNPROCESSABLE_CONTENT'
| 'TOO_MANY_REQUESTS'
| 'CLIENT_CLOSED_REQUEST'
| 'INTERNAL_SERVER_ERROR';
import type { ErrorInferenceObject, MaybePromise } from '../utils.js';

export const ACTION_ERROR_CODES = [
'BAD_REQUEST',
'UNAUTHORIZED',
'FORBIDDEN',
'NOT_FOUND',
'TIMEOUT',
'CONFLICT',
'PRECONDITION_FAILED',
'PAYLOAD_TOO_LARGE',
'UNSUPPORTED_MEDIA_TYPE',
'UNPROCESSABLE_CONTENT',
'TOO_MANY_REQUESTS',
'CLIENT_CLOSED_REQUEST',
'INTERNAL_SERVER_ERROR',
] as const;

export type ActionErrorCode = (typeof ACTION_ERROR_CODES)[number];

const codeToStatusMap: Record<ActionErrorCode, number> = {
// Implemented from tRPC error code table
Expand All @@ -40,8 +43,6 @@ const statusToCodeMap: Record<number, ActionErrorCode> = Object.entries(codeToSt
{}
);

export type ErrorInferenceObject = Record<string, any>;

export class ActionError<T extends ErrorInferenceObject = ErrorInferenceObject> extends Error {
type = 'AstroActionError';
code: ActionErrorCode = 'INTERNAL_SERVER_ERROR';
Expand Down Expand Up @@ -85,6 +86,10 @@ export class ActionError<T extends ErrorInferenceObject = ErrorInferenceObject>

export function isInputError<T extends ErrorInferenceObject>(
error?: ActionError<T>
): error is ActionInputError<T>;
export function isInputError(error?: unknown): error is ActionInputError<ErrorInferenceObject>;
export function isInputError<T extends ErrorInferenceObject>(
error?: unknown | ActionError<T>
): error is ActionInputError<T> {
return error instanceof ActionInputError;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export const ASTRO_CONFIG_DEFAULTS = {
clientPrerender: false,
globalRoutePriority: false,
rewriting: false,
env: {
validateSecrets: false,
},
},
} satisfies AstroUserConfig & { server: { open: boolean } };

Expand Down Expand Up @@ -526,6 +529,10 @@ export const AstroConfigSchema = z.object({
env: z
.object({
schema: EnvSchema.optional(),
validateSecrets: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.env.validateSecrets),
})
.strict()
.optional(),
Expand Down
17 changes: 10 additions & 7 deletions packages/astro/src/env/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,17 @@ const EnvFieldMetadata = z.union([
SecretServerEnvFieldMetadata,
]);

const KEY_REGEX = /^[A-Z_]+$/;
const EnvSchemaKey = z
.string()
.min(1)
.refine(([firstChar]) => isNaN(Number.parseInt(firstChar)), {
message: 'A valid variable name cannot start with a number.',
})
.refine((str) => /^[A-Z0-9_]+$/.test(str), {
message: 'A valid variable name can only contain uppercase letters, numbers and underscores.',
});

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)
);
export const EnvSchema = z.record(EnvSchemaKey, z.intersection(EnvFieldMetadata, EnvFieldType));

// https://www.totaltypescript.com/concepts/the-prettify-helper
type Prettify<T> = {
Expand Down
Loading

0 comments on commit 456b1c3

Please sign in to comment.