-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Actions experimental release (#10858)
* feat: port astro-actions poc * feat: basic blog example * feat: basic validationError class * feat: standard error types and safe() wrapper * refactor: move enhanceProps to astro:actions * fix: throw internal server errors * chore: refine enhance: true error message * fix: remove FormData fallback from route * refactor: clarify what enhance: true allows * feat: progressively enhanced comments * chore: changeset * refactor: enhance -> acceptFormData * wip: migrate actions to core * feat: working actions demo from astro core! * chore: changeset * chore: delete old changeset * fix: Function type lint * refactor: expose defineAction from `astro:actions` * fix: add null check to experimental * fix: export `types/actions.d.ts` * feat: more robust form data parsing * feat: support formData from rpc call * feat: remove acceptFormData flag requirement * feat: add actions.d.ts type reference on startup * refactor: actionNameProps -> getNameProps * fix: actions type import * chore: expose zod from `astro:actions` * fix: zod export path * feat: add explicit `accept` property * Use zod package instead of relative path outside of src * feat: clean up error throwing and handling flow * fix: make `accept` optional * docs: beef up actions experimental docs * fix: defineAction type narrowing on `accept` * fix: bad `getNameProps()` arg type * refactor: move to single `error` object + `isInputError()` util * fix: move res.json() parse to avoid double parse * feat: support async zod schemas * feat: serialize and expose zod properties on input error * feat: test input error in comment example * fix: remove ZodError import * fix: add actions-module to files export * fix: use workspace for test pkg versions * refactor: default export -> server export * fix: type inference for json vs. form * refactor: accept form -> defineFormAction * refactor: better callSafely signature * feat: block action calls from the server with RFC link * feat: move getActionResult to global * refactor: getNameProps -> getActionProps * refactor: body.toString() * edit: capitAl Co-authored-by: Sarah Rainsberger <[email protected]> * edit: highlight `actions` Co-authored-by: Sarah Rainsberger <[email protected]> * edit: add actions file name Co-authored-by: Sarah Rainsberger <[email protected]> * edit: not you can. You DO Co-authored-by: Sarah Rainsberger <[email protected]> * edit: declare with feeling Co-authored-by: Sarah Rainsberger <[email protected]> * edit: clarify what the `handler` does * edit: schema -> input * edit: add FormData mdn reference * edit: add defineFormAction() explainer * refactor: inline getDotAstroTypeRefs * edit: yeah yeah maybe * fix: existsSync test mock * refactor: use callSafely in middleware * test: upgradeFormData() * chore: stray console log * refactor: extract helper functions * fix: include status in error response * fix: return `undefined` when there's no action result * fix: content-type * test: e2e like button action * test: comment e2e * fix: existsSync mock for other sync test * test: action dev server raw fetch * test: build preview * chore: fix lock * fix: add dotAstroDir to existsSync * chore: slim down e2e fixture * chore: remove unneeded disabled test * refactor: better api context error * fix: return `false` for envDts * refactor: defineFormAction -> defineAction with accept * fix: check FormData on getActionProps * edit: uppercase Co-authored-by: Sarah Rainsberger <[email protected]> * fix: add switch default for 500 Co-authored-by: Emanuele Stoppa <[email protected]> * fix: add `toLowerCase()` on content-type check Co-authored-by: Emanuele Stoppa <[email protected]> * chore: use VIRTUAL_MODULE_ID for plugin * fix: remove incorrect ts-ignore * chore: remove unneeded POST method check * refactor: route callSafely * refactor: error switch case to map * chore: add link to trpc error code table * fix: add readable error on failed json.stringify * refactor: add param -> callerParam with comment * feat: always return safe from getActionResult() * refactor: move actions module to templates/ * refactor: remove unneeded existsSync on dotAstro * fix: hasContentType util for toLowerCase() * chore: comment on 415 code * refactor: upgradeFormData -> formDataToObj * fix: avoid leaking stack in production * refactor: defineProperty with write false * fix: revert package.json back to spaces * edit: use config docs for changeset * refactor: stringifiedActionsPath -> stringifiedActionsImport * fix: avoid double-handling for route * fix: support zero arg actions * refactor: move actionHandler to helper fn * fix: restore mdast deps * docs: add `output` to config --------- Co-authored-by: Sarah Rainsberger <[email protected]> Co-authored-by: Emanuele Stoppa <[email protected]> Co-authored-by: bholmesdev <[email protected]>
- Loading branch information
1 parent
6382d7d
commit c0c509b
Showing
51 changed files
with
2,320 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
--- | ||
"astro": minor | ||
--- | ||
|
||
Adds experimental support for the Actions API. Actions let you define type-safe endpoints you can query from client components with progressive enhancement built in. | ||
|
||
|
||
Actions help you write type-safe backend functions you can call from anywhere. Enable server rendering [using the `output` property](https://docs.astro.build/en/basics/rendering-modes/#on-demand-rendered) and add the `actions` flag to the `experimental` object: | ||
|
||
```js | ||
{ | ||
output: 'hybrid', // or 'server' | ||
experimental: { | ||
actions: true, | ||
}, | ||
} | ||
``` | ||
Declare all your actions in `src/actions/index.ts`. This file is the global actions handler. | ||
|
||
Define an action using the `defineAction()` utility from the `astro:actions` module. These accept the `handler` property to define your server-side request handler. If your action accepts arguments, apply the `input` property to validate parameters with Zod. | ||
|
||
This example defines two actions: `like` and `comment`. The `like` action accepts a JSON object with a `postId` string, while the `comment` action accepts [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) with `postId`, `author`, and `body` strings. Each `handler` updates your database and return a type-safe response. | ||
|
||
```ts | ||
// src/actions/index.ts | ||
import { defineAction, z } from "astro:actions"; | ||
|
||
export const server = { | ||
like: defineAction({ | ||
input: z.object({ postId: z.string() }), | ||
handler: async ({ postId }, context) => { | ||
// update likes in db | ||
|
||
return likes; | ||
}, | ||
}), | ||
comment: defineAction({ | ||
accept: 'form', | ||
input: z.object({ | ||
postId: z.string(), | ||
author: z.string(), | ||
body: z.string(), | ||
}), | ||
handler: async ({ postId }, context) => { | ||
// insert comments in db | ||
|
||
return comment; | ||
}, | ||
}), | ||
}; | ||
``` | ||
Then, call an action from your client components using the `actions` object from `astro:actions`. You can pass a type-safe object when using JSON, or a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects) object when using `accept: 'form'` in your action definition: | ||
|
||
```tsx "actions" | ||
// src/components/blog.tsx | ||
import { actions } from "astro:actions"; | ||
import { useState } from "preact/hooks"; | ||
|
||
export function Like({ postId }: { postId: string }) { | ||
const [likes, setLikes] = useState(0); | ||
return ( | ||
<button | ||
onClick={async () => { | ||
const newLikes = await actions.like({ postId }); | ||
setLikes(newLikes); | ||
}} | ||
> | ||
{likes} likes | ||
</button> | ||
); | ||
} | ||
|
||
export function Comment({ postId }: { postId: string }) { | ||
return ( | ||
<form | ||
onSubmit={async (e) => { | ||
e.preventDefault(); | ||
const formData = new FormData(e.target); | ||
const result = await actions.blog.comment(formData); | ||
// handle result | ||
}} | ||
> | ||
<input type="hidden" name="postId" value={postId} /> | ||
<label for="author">Author</label> | ||
<input id="author" type="text" name="author" /> | ||
<textarea rows={10} name="body"></textarea> | ||
<button type="submit">Post</button> | ||
</form> | ||
); | ||
} | ||
``` | ||
For a complete overview, and to give feedback on this experimental API, see the [Actions RFC](https://github.com/withastro/roadmap/blob/actions/proposals/0046-actions.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { expect } from '@playwright/test'; | ||
import { testFactory } from './test-utils.js'; | ||
|
||
const test = testFactory({ root: './fixtures/actions-blog/' }); | ||
|
||
let devServer; | ||
|
||
test.beforeAll(async ({ astro }) => { | ||
devServer = await astro.startDevServer(); | ||
}); | ||
|
||
test.afterAll(async () => { | ||
await devServer.stop(); | ||
}); | ||
|
||
test.describe('Astro Actions - Blog', () => { | ||
test('Like action', async ({ page, astro }) => { | ||
await page.goto(astro.resolveUrl('/blog/first-post/')); | ||
|
||
const likeButton = page.getByLabel('Like'); | ||
await expect(likeButton, 'like button starts with 10 likes').toContainText('10'); | ||
await likeButton.click(); | ||
await expect(likeButton, 'like button should increment likes').toContainText('11'); | ||
}); | ||
|
||
test('Comment action - validation error', async ({ page, astro }) => { | ||
await page.goto(astro.resolveUrl('/blog/first-post/')); | ||
|
||
const authorInput = page.locator('input[name="author"]'); | ||
const bodyInput = page.locator('textarea[name="body"]'); | ||
|
||
await authorInput.fill('Ben'); | ||
await bodyInput.fill('Too short'); | ||
|
||
const submitButton = page.getByLabel('Post comment'); | ||
await submitButton.click(); | ||
|
||
await expect(page.locator('p[data-error="body"]')).toBeVisible(); | ||
}); | ||
|
||
test('Comment action - success', async ({ page, astro }) => { | ||
await page.goto(astro.resolveUrl('/blog/first-post/')); | ||
|
||
const authorInput = page.locator('input[name="author"]'); | ||
const bodyInput = page.locator('textarea[name="body"]'); | ||
|
||
const body = 'This should be long enough.'; | ||
await authorInput.fill('Ben'); | ||
await bodyInput.fill(body); | ||
|
||
const submitButton = page.getByLabel('Post comment'); | ||
await submitButton.click(); | ||
|
||
const comment = await page.getByTestId('comment'); | ||
await expect(comment).toBeVisible(); | ||
await expect(comment).toContainText(body); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { defineConfig } from 'astro/config'; | ||
import db from '@astrojs/db'; | ||
import react from '@astrojs/react'; | ||
import node from '@astrojs/node'; | ||
|
||
// https://astro.build/config | ||
export default defineConfig({ | ||
site: 'https://example.com', | ||
integrations: [db(), react()], | ||
output: 'hybrid', | ||
adapter: node({ | ||
mode: 'standalone', | ||
}), | ||
experimental: { | ||
actions: true, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { column, defineDb, defineTable } from "astro:db"; | ||
|
||
const Comment = defineTable({ | ||
columns: { | ||
postId: column.text(), | ||
author: column.text(), | ||
body: column.text(), | ||
}, | ||
}); | ||
|
||
const Likes = defineTable({ | ||
columns: { | ||
postId: column.text(), | ||
likes: column.number(), | ||
}, | ||
}); | ||
|
||
// https://astro.build/db/config | ||
export default defineDb({ | ||
tables: { Comment, Likes }, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { db, Likes, Comment } from "astro:db"; | ||
|
||
// https://astro.build/db/seed | ||
export default async function seed() { | ||
await db.insert(Likes).values({ | ||
postId: "first-post.md", | ||
likes: 10, | ||
}); | ||
|
||
await db.insert(Comment).values({ | ||
postId: "first-post.md", | ||
author: "Alice", | ||
body: "Great post!", | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"name": "@e2e/astro-actions-basics", | ||
"type": "module", | ||
"version": "0.0.1", | ||
"scripts": { | ||
"dev": "astro dev", | ||
"start": "astro dev", | ||
"build": "astro check && astro build", | ||
"preview": "astro preview", | ||
"astro": "astro" | ||
}, | ||
"dependencies": { | ||
"@astrojs/check": "^0.5.10", | ||
"@astrojs/db": "workspace:*", | ||
"@astrojs/node": "workspace:*", | ||
"@astrojs/react": "workspace:*", | ||
"@types/react": "^18.2.79", | ||
"@types/react-dom": "^18.2.25", | ||
"astro": "workspace:*", | ||
"react": "^18.3.0", | ||
"react-dom": "^18.3.0", | ||
"typescript": "^5.4.5" | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { db, Comment, Likes, eq, sql } from 'astro:db'; | ||
import { defineAction, z } from 'astro:actions'; | ||
|
||
export const server = { | ||
blog: { | ||
like: defineAction({ | ||
input: z.object({ postId: z.string() }), | ||
handler: async ({ postId }) => { | ||
await new Promise((r) => setTimeout(r, 200)); | ||
|
||
const { likes } = await db | ||
.update(Likes) | ||
.set({ | ||
likes: sql`likes + 1`, | ||
}) | ||
.where(eq(Likes.postId, postId)) | ||
.returning() | ||
.get(); | ||
|
||
return likes; | ||
}, | ||
}), | ||
|
||
comment: defineAction({ | ||
accept: 'form', | ||
input: z.object({ | ||
postId: z.string(), | ||
author: z.string(), | ||
body: z.string().min(10), | ||
}), | ||
handler: async ({ postId, author, body }) => { | ||
const comment = await db | ||
.insert(Comment) | ||
.values({ | ||
postId, | ||
body, | ||
author, | ||
}) | ||
.returning() | ||
.get(); | ||
return comment; | ||
}, | ||
}), | ||
}, | ||
}; |
47 changes: 47 additions & 0 deletions
47
packages/astro/e2e/fixtures/actions-blog/src/components/BaseHead.astro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
--- | ||
// Import the global.css file here so that it is included on | ||
// all pages through the use of the <BaseHead /> component. | ||
import '../styles/global.css'; | ||
interface Props { | ||
title: string; | ||
description: string; | ||
image?: string; | ||
} | ||
const canonicalURL = new URL(Astro.url.pathname, Astro.site); | ||
const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props; | ||
--- | ||
|
||
<!-- Global Metadata --> | ||
<meta charset="utf-8" /> | ||
<meta name="viewport" content="width=device-width,initial-scale=1" /> | ||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||
<meta name="generator" content={Astro.generator} /> | ||
|
||
<!-- Font preloads --> | ||
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin /> | ||
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin /> | ||
|
||
<!-- Canonical URL --> | ||
<link rel="canonical" href={canonicalURL} /> | ||
|
||
<!-- Primary Meta Tags --> | ||
<title>{title}</title> | ||
<meta name="title" content={title} /> | ||
<meta name="description" content={description} /> | ||
|
||
<!-- Open Graph / Facebook --> | ||
<meta property="og:type" content="website" /> | ||
<meta property="og:url" content={Astro.url} /> | ||
<meta property="og:title" content={title} /> | ||
<meta property="og:description" content={description} /> | ||
<meta property="og:image" content={new URL(image, Astro.url)} /> | ||
|
||
<!-- Twitter --> | ||
<meta property="twitter:card" content="summary_large_image" /> | ||
<meta property="twitter:url" content={Astro.url} /> | ||
<meta property="twitter:title" content={title} /> | ||
<meta property="twitter:description" content={description} /> | ||
<meta property="twitter:image" content={new URL(image, Astro.url)} /> |
62 changes: 62 additions & 0 deletions
62
packages/astro/e2e/fixtures/actions-blog/src/components/Footer.astro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
--- | ||
const today = new Date(); | ||
--- | ||
|
||
<footer> | ||
© {today.getFullYear()} Your name here. All rights reserved. | ||
<div class="social-links"> | ||
<a href="https://m.webtoo.ls/@astro" target="_blank"> | ||
<span class="sr-only">Follow Astro on Mastodon</span> | ||
<svg | ||
viewBox="0 0 16 16" | ||
aria-hidden="true" | ||
width="32" | ||
height="32" | ||
astro-icon="social/mastodon" | ||
><path | ||
fill="currentColor" | ||
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z" | ||
></path></svg | ||
> | ||
</a> | ||
<a href="https://twitter.com/astrodotbuild" target="_blank"> | ||
<span class="sr-only">Follow Astro on Twitter</span> | ||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/twitter" | ||
><path | ||
fill="currentColor" | ||
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z" | ||
></path></svg | ||
> | ||
</a> | ||
<a href="https://github.com/withastro/astro" target="_blank"> | ||
<span class="sr-only">Go to Astro's GitHub repo</span> | ||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github" | ||
><path | ||
fill="currentColor" | ||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" | ||
></path></svg | ||
> | ||
</a> | ||
</div> | ||
</footer> | ||
<style> | ||
footer { | ||
padding: 2em 1em 6em 1em; | ||
background: linear-gradient(var(--gray-gradient)) no-repeat; | ||
color: rgb(var(--gray)); | ||
text-align: center; | ||
} | ||
.social-links { | ||
display: flex; | ||
justify-content: center; | ||
gap: 1em; | ||
margin-top: 1em; | ||
} | ||
.social-links a { | ||
text-decoration: none; | ||
color: rgb(var(--gray)); | ||
} | ||
.social-links a:hover { | ||
color: rgb(var(--gray-dark)); | ||
} | ||
</style> |
Oops, something went wrong.