From b2f1dddb14949ffadb903e77168e06736f2d877a Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 9 Jul 2024 10:42:10 -0400 Subject: [PATCH 1/7] feat: allow type `unknown` on `isInputError` --- .../astro/src/actions/runtime/virtual/shared.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/astro/src/actions/runtime/virtual/shared.ts b/packages/astro/src/actions/runtime/virtual/shared.ts index 420682aad4fc..704c7eb3a3b9 100644 --- a/packages/astro/src/actions/runtime/virtual/shared.ts +++ b/packages/astro/src/actions/runtime/virtual/shared.ts @@ -40,6 +40,15 @@ const statusToCodeMap: Record = Object.entries(codeToSt {} ); +/** + * 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; export class ActionError extends Error { @@ -85,6 +94,10 @@ export class ActionError export function isInputError( error?: ActionError +): error is ActionInputError; +export function isInputError(error?: unknown): error is ActionInputError; +export function isInputError( + error?: unknown | ActionError ): error is ActionInputError { return error instanceof ActionInputError; } From c4feb7ffd1a71930fb97e64e05d80a418df15ab4 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 9 Jul 2024 10:43:20 -0400 Subject: [PATCH 2/7] chore: move ErrorInferenceObject to internal utils --- packages/astro/src/actions/runtime/utils.ts | 11 +++++++++++ .../astro/src/actions/runtime/virtual/server.ts | 10 ++-------- .../astro/src/actions/runtime/virtual/shared.ts | 13 +------------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/astro/src/actions/runtime/utils.ts b/packages/astro/src/actions/runtime/utils.ts index eac9b92cf99e..02961144b4e2 100644 --- a/packages/astro/src/actions/runtime/utils.ts +++ b/packages/astro/src/actions/runtime/utils.ts @@ -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; diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts index ce4d5f69662b..4d3745a6874d 100644 --- a/packages/astro/src/actions/runtime/virtual/server.ts +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -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'; diff --git a/packages/astro/src/actions/runtime/virtual/shared.ts b/packages/astro/src/actions/runtime/virtual/shared.ts index 704c7eb3a3b9..94a22f7ca388 100644 --- a/packages/astro/src/actions/runtime/virtual/shared.ts +++ b/packages/astro/src/actions/runtime/virtual/shared.ts @@ -1,5 +1,5 @@ import type { z } from 'zod'; -import type { MaybePromise } from '../utils.js'; +import type { ErrorInferenceObject, MaybePromise } from '../utils.js'; type ActionErrorCode = | 'BAD_REQUEST' @@ -40,17 +40,6 @@ const statusToCodeMap: Record = Object.entries(codeToSt {} ); -/** - * 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; - export class ActionError extends Error { type = 'AstroActionError'; code: ActionErrorCode = 'INTERNAL_SERVER_ERROR'; From 33fcb51827f58fa8bcff76b368ffa3fbe725b327 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 9 Jul 2024 11:02:51 -0400 Subject: [PATCH 3/7] chore: changeset --- .changeset/nasty-poems-juggle.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .changeset/nasty-poems-juggle.md diff --git a/.changeset/nasty-poems-juggle.md b/.changeset/nasty-poems-juggle.md new file mode 100644 index 000000000000..74e1b176d036 --- /dev/null +++ b/.changeset/nasty-poems-juggle.md @@ -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); + } +} +``` From 916898b50e66753ca483b75db16df80e17322782 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 9 Jul 2024 14:42:57 -0400 Subject: [PATCH 4/7] deps: expect-type --- packages/astro/package.json | 1 + pnpm-lock.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/astro/package.json b/packages/astro/package.json index 164d155a70d5..b3c60178e3e6 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -215,6 +215,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5e9bf3dda3b..934cd36fcb26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -794,6 +794,9 @@ importers: eol: specifier: ^0.9.1 version: 0.9.1 + expect-type: + specifier: ^0.19.0 + version: 0.19.0 mdast-util-mdx: specifier: ^3.0.0 version: 3.0.0 @@ -8654,6 +8657,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + expect-type@0.19.0: + resolution: {integrity: sha512-piv9wz3IrAG4Wnk2A+n2VRCHieAyOSxrRLU872Xo6nyn39kYXKDALk4OcqnvLRnFvkz659CnWC8MWZLuuQnoqg==} + engines: {node: '>=12.0.0'} + express@4.19.2: resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} @@ -9308,6 +9315,7 @@ packages: libsql@0.3.12: resolution: {integrity: sha512-to30hj8O3DjS97wpbKN6ERZ8k66MN1IaOfFLR6oHqd25GMiPJ/ZX0VaZ7w+TsPmxcFS3p71qArj/hiedCyvXCg==} + cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] lilconfig@2.1.0: @@ -14704,6 +14712,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + expect-type@0.19.0: {} + express@4.19.2: dependencies: accepts: 1.3.8 From b60a546db9c5d7941bc7dd0d7a2abb37b6ccde38 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 9 Jul 2024 14:45:06 -0400 Subject: [PATCH 5/7] feat: first types test --- package.json | 1 + packages/astro/package.json | 1 + packages/astro/test/types/is-input-error.ts | 26 +++++++++++++++++++++ packages/astro/tsconfig.tests.json | 9 +++++++ 4 files changed, 37 insertions(+) create mode 100644 packages/astro/test/types/is-input-error.ts create mode 100644 packages/astro/tsconfig.tests.json diff --git a/package.json b/package.json index 4d477912b2ba..e99e740da097 100644 --- a/package.json +++ b/package.json @@ -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/*\"", diff --git a/packages/astro/package.json b/packages/astro/package.json index b3c60178e3e6..7ebd31e2bea6 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -120,6 +120,7 @@ "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": { diff --git a/packages/astro/test/types/is-input-error.ts b/packages/astro/test/types/is-input-error.ts new file mode 100644 index 000000000000..b8f829d5f20b --- /dev/null +++ b/packages/astro/test/types/is-input-error.ts @@ -0,0 +1,26 @@ +import { expectTypeOf } from 'expect-type'; +import { isInputError, defineAction } from '../../dist/actions/runtime/virtual/server.js'; +import { z } from '../../zod.mjs'; + +const exampleAction = defineAction({ + input: z.object({ + name: z.string(), + }), + handler: () => {}, +}); + +const result = await exampleAction.safe({ name: 'Alice' }); + +// `isInputError` narrows unknown error types +try { + await exampleAction({ name: 'Alice' }); +} catch (e) { + if (isInputError(e)) { + expectTypeOf(e.fields).toEqualTypeOf>(); + } +} + +// `isInputError` preserves `fields` object type for ActionError objects +if (isInputError(result.error)) { + expectTypeOf(result.error.fields).toEqualTypeOf<{ name?: string[] }>(); +} diff --git a/packages/astro/tsconfig.tests.json b/packages/astro/tsconfig.tests.json new file mode 100644 index 000000000000..1178731fa2c9 --- /dev/null +++ b/packages/astro/tsconfig.tests.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test/types"], + "compilerOptions": { + "allowJs": true, + "emitDeclarationOnly": false, + "noEmit": true, + } +} From b6191711ea49ba749922b252fcfcdd700d15008e Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 9 Jul 2024 14:48:14 -0400 Subject: [PATCH 6/7] chore: add types test to general test command --- packages/astro/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index 7ebd31e2bea6..c66cb301e39e 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -114,7 +114,7 @@ "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", From ddf1bb060534298976dd0bd7eceaf91fc09cdb80 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Tue, 9 Jul 2024 14:55:25 -0400 Subject: [PATCH 7/7] refactor: use describe and it for organization --- packages/astro/test/types/is-input-error.ts | 29 ++++++++++++--------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/astro/test/types/is-input-error.ts b/packages/astro/test/types/is-input-error.ts index b8f829d5f20b..ba0f7c0bc42f 100644 --- a/packages/astro/test/types/is-input-error.ts +++ b/packages/astro/test/types/is-input-error.ts @@ -1,6 +1,7 @@ import { expectTypeOf } from 'expect-type'; import { isInputError, defineAction } from '../../dist/actions/runtime/virtual/server.js'; import { z } from '../../zod.mjs'; +import { describe, it } from 'node:test'; const exampleAction = defineAction({ input: z.object({ @@ -11,16 +12,20 @@ const exampleAction = defineAction({ const result = await exampleAction.safe({ name: 'Alice' }); -// `isInputError` narrows unknown error types -try { - await exampleAction({ name: 'Alice' }); -} catch (e) { - if (isInputError(e)) { - expectTypeOf(e.fields).toEqualTypeOf>(); - } -} +describe('isInputError', () => { + it('isInputError narrows unknown error types', async () => { + try { + await exampleAction({ name: 'Alice' }); + } catch (e) { + if (isInputError(e)) { + expectTypeOf(e.fields).toEqualTypeOf>(); + } + } + }); -// `isInputError` preserves `fields` object type for ActionError objects -if (isInputError(result.error)) { - expectTypeOf(result.error.fields).toEqualTypeOf<{ name?: string[] }>(); -} + it('`isInputError` preserves `fields` object type for ActionError objects', async () => { + if (isInputError(result.error)) { + expectTypeOf(result.error.fields).toEqualTypeOf<{ name?: string[] }>(); + } + }); +});