From 1d973dc83394bd119890e681493c0351a2eff7fa Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Wed, 28 Aug 2024 17:03:35 -0400 Subject: [PATCH] Astro Actions (#8980) Co-authored-by: Reuben Tier <64310361+TheOtterlord@users.noreply.github.com> Co-authored-by: Armand Philippot Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> Co-authored-by: Sarah Rainsberger Co-authored-by: Chris Swithinbank --- astro.config.ts | 4 + package.json | 1 + pnpm-lock.yaml | 233 ++----- src/content/docs/en/guides/actions.mdx | 580 ++++++++++++++++++ .../docs/en/reference/api-reference.mdx | 188 +++++- src/i18n/en/nav.ts | 1 + 6 files changed, 839 insertions(+), 168 deletions(-) create mode 100644 src/content/docs/en/guides/actions.mdx diff --git a/astro.config.ts b/astro.config.ts index ea457e6f3df57..b55dc5e7ae324 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -1,4 +1,5 @@ import starlight from '@astrojs/starlight'; +import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'; import { defineConfig, sharpImageService } from 'astro/config'; import { makeLocalesConfig } from './config/locales'; import { makeSidebar } from './config/sidebar'; @@ -23,6 +24,9 @@ export default defineConfig({ starlight({ title: 'Docs', customCss: ['./src/styles/custom.css'], + expressiveCode: { + plugins: [pluginCollapsibleSections()], + }, components: { EditLink: './src/components/starlight/EditLink.astro', Head: './src/components/starlight/Head.astro', diff --git a/package.json b/package.json index 4febb2a005991..941d578851d92 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@astrojs/sitemap": "^3.1.5", "@astrojs/starlight": "^0.26.1", "@docsearch/js": "^3.5.2", + "@expressive-code/plugin-collapsible-sections": "^0.35.0", "@fontsource/ibm-plex-mono": "^4.5.10", "@lunariajs/core": "^0.1.1", "canvas-confetti": "^1.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be755b967b59d..3b48df19620d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,13 +13,16 @@ importers: version: 0.7.0(prettier-plugin-astro@0.12.2)(prettier@3.1.0)(typescript@5.0.2) '@astrojs/sitemap': specifier: ^3.1.5 - version: 3.1.5 + version: 3.1.6 '@astrojs/starlight': specifier: ^0.26.1 version: 0.26.1(astro@4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2)) '@docsearch/js': specifier: ^3.5.2 version: 3.5.2(@algolia/client-search@4.23.3)(search-insights@2.13.0) + '@expressive-code/plugin-collapsible-sections': + specifier: ^0.35.0 + version: 0.35.6 '@fontsource/ibm-plex-mono': specifier: ^4.5.10 version: 4.5.10 @@ -68,13 +71,13 @@ importers: version: 1.6.0 '@types/hast': specifier: ^3.0.3 - version: 3.0.3 + version: 3.0.4 '@types/html-escaper': specifier: ^3.0.0 version: 3.0.0 '@types/mdast': specifier: ^4.0.3 - version: 4.0.3 + version: 4.0.4 '@types/node': specifier: ^18.6.4 version: 18.6.4 @@ -197,7 +200,7 @@ importers: version: 5.0.2 unified: specifier: ^11.0.4 - version: 11.0.4 + version: 11.0.5 unist-util-remove: specifier: ^4.0.0 version: 4.0.0 @@ -340,8 +343,8 @@ packages: '@astrojs/markdown-remark@5.2.0': resolution: {integrity: sha512-vWGM24KZXz11jR3JO+oqYU3T2qpuOi4uGivJ9SQLCAI01+vEkHC60YJMRvHPc+hwd60F7euNs1PeOEixIIiNQw==} - '@astrojs/mdx@3.1.3': - resolution: {integrity: sha512-hOM4dMM4RfJI254d3p/AnOZuk2VyKszRtuY5FBm+Xc4XdhIpGrR56OXMNEcWchtwz4HQyPe/eJSgvBjSROcQIQ==} + '@astrojs/mdx@3.1.4': + resolution: {integrity: sha512-AcdcAlDpzTM5LHpur7A3NWoIqyfhH1gZNbTvvjiUlDEo7eJjIxl4gdWrb/kZZRfLBEuM8cptCB+Qk11ncQL4IA==} engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} peerDependencies: astro: ^4.8.0 @@ -350,9 +353,6 @@ packages: resolution: {integrity: sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==} engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} - '@astrojs/sitemap@3.1.5': - resolution: {integrity: sha512-GLdzJ01387Uzb8RKYpsYLlg/GzoPnGbmDeQNkarSE11i2+l9Qp8Nj/WoTEy9nkTS25fxxy0kxDfJmreeVleCqg==} - '@astrojs/sitemap@3.1.6': resolution: {integrity: sha512-1Qp2NvAzVImqA6y+LubKi1DVhve/hXXgFvB0szxiipzh7BvtuKe4oJJ9dXSqaubaTkt4nMa6dv6RCCAYeB6xaQ==} @@ -683,6 +683,9 @@ packages: '@expressive-code/core@0.35.6': resolution: {integrity: sha512-xGqCkmfkgT7lr/rvmfnYdDSeTdCSp1otAHgoFS6wNEeO7wGDPpxdosVqYiIcQ8CfWUABh/pGqWG90q+MV3824A==} + '@expressive-code/plugin-collapsible-sections@0.35.6': + resolution: {integrity: sha512-PciZoBynxp3DCrK3dvcc/rxkj2HVbFxX992yqez1pircmPj0g1STySslkOBVMHh9COy6whJ4Mbq2k9DPV1A5/Q==} + '@expressive-code/plugin-frames@0.35.6': resolution: {integrity: sha512-CqjSWjDJ3wabMJZfL9ZAzH5UAGKg7KWsf1TBzr4xvUbZvWoBtLA/TboBML0U1Ls8h/4TRCIvR4VEb8dv5+QG3w==} @@ -978,9 +981,6 @@ packages: '@shikijs/core@1.14.1': resolution: {integrity: sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==} - '@shikijs/core@1.7.0': - resolution: {integrity: sha512-O6j27b7dGmJbR3mjwh/aHH8Ld+GQvA0OQsNO43wKWnqbAae3AYXrhFyScHGX8hXZD6vX2ngjzDFkZY5srtIJbQ==} - '@ts-morph/common@0.16.0': resolution: {integrity: sha512-SgJpzkTgZKLKqQniCjLaE3c2L2sdL7UShvmTmPBejAKd2OKV/yfMpQ2IWpAuA+VY5wy7PkSUaEObIqEK6afFuw==} @@ -1014,9 +1014,6 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/hast@3.0.3': - resolution: {integrity: sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -1032,9 +1029,6 @@ packages: '@types/markdown-it@12.2.3': resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} - '@types/mdast@4.0.3': - resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1181,16 +1175,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.12.0: - resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.12.1: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} @@ -1547,15 +1531,6 @@ packages: resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==} engines: {node: '>= 12'} - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.5: resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} engines: {node: '>=6.0'} @@ -1699,9 +1674,6 @@ packages: resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} engines: {node: '>= 0.4'} - es-module-lexer@1.5.3: - resolution: {integrity: sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==} - es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} @@ -2282,9 +2254,6 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} - import-meta-resolve@4.0.0: - resolution: {integrity: sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==} - import-meta-resolve@4.1.0: resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} @@ -3347,9 +3316,6 @@ packages: shiki@1.14.1: resolution: {integrity: sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==} - shiki@1.7.0: - resolution: {integrity: sha512-H5pMn4JA7ayx8H0qOz1k2qANq6mZVCMl1gKLK6kWIrv1s2Ial4EmD4s4jE8QB5Dw03d/oCQUxc24sotuyR5byA==} - side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} @@ -3378,11 +3344,6 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - sitemap@7.1.1: - resolution: {integrity: sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==} - engines: {node: '>=12.0.0', npm: '>=5.6.0'} - hasBin: true - sitemap@7.1.2: resolution: {integrity: sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==} engines: {node: '>=12.0.0', npm: '>=5.6.0'} @@ -3642,9 +3603,6 @@ packages: unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} - unified@11.0.4: - resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -3730,9 +3688,6 @@ packages: vfile@5.3.7: resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} - vfile@6.0.1: - resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} - vfile@6.0.2: resolution: {integrity: sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==} @@ -3959,7 +3914,7 @@ snapshots: '@11ty/eleventy-fetch@3.0.0': dependencies: - debug: 4.3.4 + debug: 4.3.5 flat-cache: 3.0.4 node-fetch: 2.6.7 p-queue: 6.6.2 @@ -4143,7 +4098,7 @@ snapshots: github-slugger: 2.0.0 hast-util-from-html: 2.0.1 hast-util-to-text: 4.0.2 - import-meta-resolve: 4.0.0 + import-meta-resolve: 4.1.0 mdast-util-definitions: 6.0.0 rehype-raw: 7.0.0 rehype-stringify: 10.0.0 @@ -4151,12 +4106,12 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.0 remark-smartypants: 2.0.0 - shiki: 1.7.0 - unified: 11.0.4 + shiki: 1.14.1 + unified: 11.0.5 unist-util-remove-position: 5.0.0 unist-util-visit: 5.0.0 unist-util-visit-parents: 6.0.1 - vfile: 6.0.1 + vfile: 6.0.2 transitivePeerDependencies: - supports-color @@ -4183,7 +4138,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@3.1.3(astro@4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2))': + '@astrojs/mdx@3.1.4(astro@4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2))': dependencies: '@astrojs/markdown-remark': 5.2.0 '@mdx-js/mdx': 3.0.1 @@ -4191,7 +4146,6 @@ snapshots: astro: 4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2) es-module-lexer: 1.5.4 estree-util-visit: 2.0.0 - github-slugger: 2.0.0 gray-matter: 4.0.3 hast-util-to-html: 9.0.1 kleur: 4.1.5 @@ -4208,12 +4162,6 @@ snapshots: dependencies: prismjs: 1.29.0 - '@astrojs/sitemap@3.1.5': - dependencies: - sitemap: 7.1.1 - stream-replace-string: 2.0.0 - zod: 3.23.8 - '@astrojs/sitemap@3.1.6': dependencies: sitemap: 7.1.2 @@ -4222,7 +4170,7 @@ snapshots: '@astrojs/starlight@0.26.1(astro@4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2))': dependencies: - '@astrojs/mdx': 3.1.3(astro@4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2)) + '@astrojs/mdx': 3.1.4(astro@4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2)) '@astrojs/sitemap': 3.1.6 '@pagefind/default-ui': 1.0.3 '@types/hast': 3.0.4 @@ -4279,7 +4227,7 @@ snapshots: '@babel/traverse': 7.24.7 '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.5 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4403,7 +4351,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.24.7 '@babel/types': 7.24.7 - debug: 4.3.4 + debug: 4.3.5 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4551,7 +4499,7 @@ snapshots: '@eslint/eslintrc@1.3.3': dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.5 espree: 9.4.1 globals: 13.15.0 ignore: 5.2.0 @@ -4574,6 +4522,10 @@ snapshots: unist-util-visit: 5.0.0 unist-util-visit-parents: 6.0.1 + '@expressive-code/plugin-collapsible-sections@0.35.6': + dependencies: + '@expressive-code/core': 0.35.6 + '@expressive-code/plugin-frames@0.35.6': dependencies: '@expressive-code/core': 0.35.6 @@ -4581,7 +4533,7 @@ snapshots: '@expressive-code/plugin-shiki@0.35.6': dependencies: '@expressive-code/core': 0.35.6 - shiki: 1.7.0 + shiki: 1.14.1 '@expressive-code/plugin-text-markers@0.35.6': dependencies: @@ -4592,7 +4544,7 @@ snapshots: '@humanwhocodes/config-array@0.11.7': dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.3.5 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4838,8 +4790,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - '@shikijs/core@1.7.0': {} - '@ts-morph/common@0.16.0': dependencies: fast-glob: 3.3.2 @@ -4886,10 +4836,6 @@ snapshots: '@types/estree@1.0.5': {} - '@types/hast@3.0.3': - dependencies: - '@types/unist': 3.0.2 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.2 @@ -4905,10 +4851,6 @@ snapshots: '@types/linkify-it': 3.0.2 '@types/mdurl': 1.0.2 - '@types/mdast@4.0.3': - dependencies: - '@types/unist': 3.0.2 - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.2 @@ -4953,7 +4895,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.46.1 '@typescript-eslint/type-utils': 5.46.1(eslint@8.29.0)(typescript@5.0.2) '@typescript-eslint/utils': 5.46.1(eslint@8.29.0)(typescript@5.0.2) - debug: 4.3.4 + debug: 4.3.5 eslint: 8.29.0 ignore: 5.2.0 natural-compare-lite: 1.4.0 @@ -4970,7 +4912,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.46.1 '@typescript-eslint/types': 5.46.1 '@typescript-eslint/typescript-estree': 5.46.1(typescript@5.0.2) - debug: 4.3.4 + debug: 4.3.5 eslint: 8.29.0 optionalDependencies: typescript: 5.0.2 @@ -4986,7 +4928,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.46.1(typescript@5.0.2) '@typescript-eslint/utils': 5.46.1(eslint@8.29.0)(typescript@5.0.2) - debug: 4.3.4 + debug: 4.3.5 eslint: 8.29.0 tsutils: 3.21.0(typescript@5.0.2) optionalDependencies: @@ -5000,7 +4942,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.46.1 '@typescript-eslint/visitor-keys': 5.46.1 - debug: 4.3.4 + debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.2 @@ -5094,18 +5036,10 @@ snapshots: '@webgpu/types@0.1.21': {} - acorn-jsx@5.3.2(acorn@8.12.0): - dependencies: - acorn: 8.12.0 - acorn-jsx@5.3.2(acorn@8.12.1): dependencies: acorn: 8.12.1 - acorn@8.11.3: {} - - acorn@8.12.0: {} - acorn@8.12.1: {} ajv@6.12.6: @@ -5211,7 +5145,7 @@ snapshots: astro-auto-import@0.4.2(astro@4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2)): dependencies: '@types/node': 18.6.4 - acorn: 8.11.3 + acorn: 8.12.1 astro: 4.11.0(@types/node@18.6.4)(sass@1.54.3)(typescript@5.0.2) astro-eslint-parser@0.16.0: @@ -5220,7 +5154,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.46.1 '@typescript-eslint/types': 5.46.1 astrojs-compiler-sync: 0.3.1(@astrojs/compiler@2.8.0) - debug: 4.3.4 + debug: 4.3.5 eslint-visitor-keys: 3.3.0 espree: 9.4.1 semver: 7.6.2 @@ -5232,7 +5166,7 @@ snapshots: '@astrojs/compiler': 0.31.0 '@typescript-eslint/types': 5.46.1 astrojs-compiler-sync: 0.3.1(@astrojs/compiler@0.31.0) - debug: 4.3.4 + debug: 4.3.5 eslint-scope: 7.1.1 eslint-visitor-keys: 3.3.0 espree: 9.4.1 @@ -5265,7 +5199,7 @@ snapshots: '@babel/types': 7.24.7 '@types/babel__core': 7.20.5 '@types/cookie': 0.6.0 - acorn: 8.12.0 + acorn: 8.12.1 aria-query: 5.3.0 axobject-query: 4.0.0 boxen: 7.1.1 @@ -5281,7 +5215,7 @@ snapshots: diff: 5.2.0 dlv: 1.1.3 dset: 3.1.3 - es-module-lexer: 1.5.3 + es-module-lexer: 1.5.4 esbuild: 0.21.5 estree-walker: 3.0.3 execa: 8.0.1 @@ -5304,12 +5238,12 @@ snapshots: rehype: 13.0.1 resolve: 1.22.8 semver: 7.6.2 - shiki: 1.7.0 + shiki: 1.14.1 string-width: 7.1.0 strip-ansi: 7.1.0 tsconfck: 3.1.0(typescript@5.0.2) unist-util-visit: 5.0.0 - vfile: 6.0.1 + vfile: 6.0.2 vite: 5.3.1(@types/node@18.6.4)(sass@1.54.3) vitefu: 0.2.5(vite@5.3.1(@types/node@18.6.4)(sass@1.54.3)) which-pm: 2.2.0 @@ -5563,10 +5497,6 @@ snapshots: data-uri-to-buffer@4.0.0: {} - debug@4.3.4: - dependencies: - ms: 2.1.2 - debug@4.3.5: dependencies: ms: 2.1.2 @@ -5726,8 +5656,6 @@ snapshots: unbox-primitive: 1.0.2 which-typed-array: 1.1.9 - es-module-lexer@1.5.3: {} - es-module-lexer@1.5.4: {} es-set-tostringtag@2.0.1: @@ -5926,7 +5854,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.5 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 @@ -5963,8 +5891,8 @@ snapshots: espree@9.4.1: dependencies: - acorn: 8.12.0 - acorn-jsx: 5.3.2(acorn@8.12.0) + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) eslint-visitor-keys: 3.3.0 esprima@4.0.1: {} @@ -6263,11 +6191,11 @@ snapshots: hast-util-from-html@2.0.1: dependencies: - '@types/hast': 3.0.3 + '@types/hast': 3.0.4 devlop: 1.1.0 hast-util-from-parse5: 8.0.1 parse5: 7.1.2 - vfile: 6.0.1 + vfile: 6.0.2 vfile-message: 4.0.2 hast-util-from-parse5@8.0.1: @@ -6327,7 +6255,7 @@ snapshots: hast-util-select@6.0.2: dependencies: - '@types/hast': 3.0.3 + '@types/hast': 3.0.4 '@types/unist': 3.0.2 bcp-47-match: 2.0.2 comma-separated-tokens: 2.0.3 @@ -6367,7 +6295,7 @@ snapshots: hast-util-to-html@9.0.1: dependencies: - '@types/hast': 3.0.3 + '@types/hast': 3.0.4 '@types/unist': 3.0.2 ccount: 2.0.1 comma-separated-tokens: 2.0.3 @@ -6412,7 +6340,7 @@ snapshots: hast-util-to-string@3.0.0: dependencies: - '@types/hast': 3.0.3 + '@types/hast': 3.0.4 hast-util-to-text@4.0.2: dependencies: @@ -6427,7 +6355,7 @@ snapshots: hastscript@8.0.0: dependencies: - '@types/hast': 3.0.3 + '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 hast-util-parse-selector: 4.0.0 property-information: 6.2.0 @@ -6473,8 +6401,6 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-meta-resolve@4.0.0: {} - import-meta-resolve@4.1.0: {} imurmurhash@0.1.4: {} @@ -6796,7 +6722,7 @@ snapshots: mdast-util-from-markdown@2.0.0: dependencies: - '@types/mdast': 4.0.3 + '@types/mdast': 4.0.4 '@types/unist': 3.0.2 decode-named-character-reference: 1.0.2 devlop: 1.1.0 @@ -6925,8 +6851,8 @@ snapshots: mdast-util-to-hast@13.0.2: dependencies: - '@types/hast': 3.0.3 - '@types/mdast': 4.0.3 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 '@ungap/structured-clone': 1.2.0 devlop: 1.1.0 micromark-util-sanitize-uri: 2.0.0 @@ -6947,7 +6873,7 @@ snapshots: mdast-util-to-string@4.0.0: dependencies: - '@types/mdast': 4.0.3 + '@types/mdast': 4.0.4 mdurl@1.0.1: {} @@ -7210,7 +7136,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.7 - debug: 4.3.4 + debug: 4.3.5 decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 @@ -7629,11 +7555,11 @@ snapshots: rehype-autolink-headings@7.1.0: dependencies: - '@types/hast': 3.0.3 + '@types/hast': 3.0.4 '@ungap/structured-clone': 1.2.0 hast-util-heading-rank: 3.0.0 hast-util-is-element: 3.0.0 - unified: 11.0.4 + unified: 11.0.5 unist-util-visit: 5.0.0 rehype-expressive-code@0.35.6: @@ -7663,7 +7589,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 hast-util-from-html: 2.0.1 - unified: 11.0.4 + unified: 11.0.5 rehype-raw@7.0.0: dependencies: @@ -7673,7 +7599,7 @@ snapshots: rehype-slug@6.0.0: dependencies: - '@types/hast': 3.0.3 + '@types/hast': 3.0.4 github-slugger: 2.0.0 hast-util-heading-rank: 3.0.0 hast-util-to-string: 3.0.0 @@ -7687,28 +7613,28 @@ snapshots: rehype@13.0.1: dependencies: - '@types/hast': 3.0.3 + '@types/hast': 3.0.4 rehype-parse: 9.0.0 rehype-stringify: 10.0.0 - unified: 11.0.4 + unified: 11.0.5 remark-directive@3.0.0: dependencies: - '@types/mdast': 4.0.3 + '@types/mdast': 4.0.4 mdast-util-directive: 3.0.0 micromark-extension-directive: 3.0.0 - unified: 11.0.4 + unified: 11.0.5 transitivePeerDependencies: - supports-color remark-gfm@4.0.0: dependencies: - '@types/mdast': 4.0.3 + '@types/mdast': 4.0.4 mdast-util-gfm: 3.0.0 micromark-extension-gfm: 3.0.0 remark-parse: 11.0.0 remark-stringify: 11.0.0 - unified: 11.0.4 + unified: 11.0.5 transitivePeerDependencies: - supports-color @@ -7753,14 +7679,14 @@ snapshots: dependencies: '@types/mdast': 4.0.4 mdast-util-to-markdown: 2.1.0 - unified: 11.0.4 + unified: 11.0.5 remark@15.0.1: dependencies: - '@types/mdast': 4.0.3 + '@types/mdast': 4.0.4 remark-parse: 11.0.0 remark-stringify: 11.0.0 - unified: 11.0.4 + unified: 11.0.5 transitivePeerDependencies: - supports-color @@ -7964,10 +7890,6 @@ snapshots: '@shikijs/core': 1.14.1 '@types/hast': 3.0.4 - shiki@1.7.0: - dependencies: - '@shikijs/core': 1.7.0 - side-channel@1.0.4: dependencies: call-bind: 1.0.2 @@ -8002,13 +7924,6 @@ snapshots: sisteransi@1.0.5: {} - sitemap@7.1.1: - dependencies: - '@types/node': 17.0.45 - '@types/sax': 1.2.4 - arg: 5.0.2 - sax: 1.2.4 - sitemap@7.1.2: dependencies: '@types/node': 17.0.45 @@ -8271,16 +8186,6 @@ snapshots: trough: 2.1.0 vfile: 5.3.7 - unified@11.0.4: - dependencies: - '@types/unist': 3.0.2 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.0.0 - trough: 2.1.0 - vfile: 6.0.1 - unified@11.0.5: dependencies: '@types/unist': 3.0.2 @@ -8409,12 +8314,6 @@ snapshots: unist-util-stringify-position: 3.0.3 vfile-message: 3.1.4 - vfile@6.0.1: - dependencies: - '@types/unist': 3.0.2 - unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 - vfile@6.0.2: dependencies: '@types/unist': 3.0.2 diff --git a/src/content/docs/en/guides/actions.mdx b/src/content/docs/en/guides/actions.mdx new file mode 100644 index 0000000000000..98a9262cf9ada --- /dev/null +++ b/src/content/docs/en/guides/actions.mdx @@ -0,0 +1,580 @@ +--- +title: Actions +description: Learn how to create type-safe server functions you can call from anywhere. +i18nReady: true +--- + +import { Steps } from '@astrojs/starlight/components'; +import Since from '~/components/Since.astro'; +import ReadMore from '~/components/ReadMore.astro'; + +

+ +Astro Actions allow you to define and call backend functions with type-safety. Actions perform data fetching, JSON parsing, and input validation for you. This can greatly reduce the amount of boilerplate needed compared to using an [API endpoint](/en/guides/endpoints/). + +Use actions instead of API endpoints for seamless communication between your client and server code and to: + +- Automatically validate JSON and form data inputs using [Zod validaton](https://zod.dev/?id=primitives). +- Generate type-safe functions to call your backend from the client and even [from HTML form actions](#call-actions-from-an-html-form-action). No need for manual `fetch()` calls. +- Standardize backend errors with the [`ActionError`](/en/reference/api-reference/#actionerror) object. + +## Basic usage + +Actions are defined in a `server` object exported from `src/actions/index.ts`: + +```ts title="src/actions/index.ts" +import { defineAction } from 'astro:actions'; +import { z } from 'astro:schema'; + +export const server = { + myAction: defineAction({ /* ... */ }) +} +``` + +Your actions are available as functions from the `astro:actions` module. Import `actions` and call them client-side within a [UI framework component](/en/guides/framework-components/), [a form POST request](#call-actions-from-an-html-form-action), or by using a ` +``` + +### Write your first action + +Follow these steps to define an action and call it in a `script` tag in your Astro page. + + + +1. Create a `src/actions/index.ts` file and export a `server` object. + + ```ts title="src/actions/index.ts" + export const server = { + // action declarations + } + ``` + +2. Import the `defineAction()` utility from `astro:actions`, and the `z` object from `astro:schema`. + + ```ts ins={1-2} title="src/actions/index.ts" + import { defineAction } from 'astro:actions'; + import { z } from 'astro:schema'; + + export const server = { + // action declarations + } + +3. Use the `defineAction()` utility to define a `getGreeting` action. The `input` property will be used to validate input parameters with a [Zod](https://zod.dev) schema and the `handler()` function includes the backend logic to run on the server. + + ```ts ins={5-12} title="src/actions/index.ts" + import { defineAction } from 'astro:actions'; + import { z } from 'astro:schema'; + + export const server = { + getGreeting: defineAction({ + input: z.object({ + name: z.string(), + }), + handler: async (input) => { + return `Hello, ${input.name}!` + } + }) + } + ``` + +4. Create an Astro component with a button that will fetch a greeting using your `getGreeting` action when clicked. + + ```astro title="src/pages/index.astro" + --- + --- + + + + + ``` + +5. To use your action, import `actions` from `astro:actions` and then call `actions.getGreeting()` in the click handler. The `name` option will be sent to your action’s `handler()` on the server and, if there are no errors, the result will be available as the `data` property. + + ```astro title="src/pages/index.astro" ins={7, 12-13} + --- + --- + + + + + ``` + + + +See the full Actions API documentation for details on [`defineAction()`](/en/reference/api-reference/#defineaction) and its properties. + +## Organizing actions + +All actions in your project must be exported from the `server` object in the `src/actions/index.ts` file. You can define actions inline or you can move action definitions to separate files and import them. You can even group related functions in nested objects. + +For example, to colocate all of your user actions, you can create a `src/actions/user.ts` file and nest the definitions of both `getUser` and `createUser` inside a single `user` object. + +```ts +// src/actions/user.ts +import { defineAction } from 'astro:actions'; + +export const user = { + getUser: defineAction(/* ... */), + createUser: defineAction(/* ... */), +} +``` + +Then, you can import this `user` object into your `src/actions/index.ts` file and add it as a top-level key to the `server` object alongside any other actions: + +```ts title="src/actions/index.ts" ins={1,5} +import { user } from './user'; + +export const server = { + myAction: defineAction({ /* ... */ }), + user, +} +``` + +Now, all of your user actions are callable from the `actions.user` object: + +- `actions.user.getUser()` +- `actions.user.createUser()` + + +## Handling returned data + +Actions return an object containing either `data` with the type-safe return value of your `handler()`, or an `error` with any backend errors. Errors may come from validation errors on the `input` property or thrown errors within the `handler()`. + +### Checking for errors + +It's best to check if an `error` is present before using the `data` property. This allows you to handle errors in advance and ensures `data` is defined without an `undefined` check. + +```ts +const { data, error } = await actions.example(); + +if (error) { + // handle error cases + return; +} +// use `data` +``` + +### Accessing `data` directly without an error check + +To skip error handling, for example while prototyping or using a library that will catch errors for you, use the `.orThrow()` property on your action call to throw errors instead of returning an `error`. This will return the action's `data` directly. + +This example calls a `likePost()` action that returns the updated number of likes as a `number` from the action `handler`: + +```ts ins="orThrow" +const updatedLikes = await actions.likePost.orThrow({ postId: 'example' }); +// ^ type: number +``` + +### Handling backend errors in your action + +You can use the provided `ActionError` to throw an error from your action `handler()`, such as "not found" when a database entry is missing, or "unauthorized" when a user is not logged in. This has two main benefits over returning `undefined`: + + +- You can set a status code like `404 - Not found` or `401 - Unauthorized`. This improves debugging errors in both development and in production by letting you see the status code of each request. + +- In your application code, all errors are passed to the `error` object on an action result. This avoids the need for `undefined` checks on data, and allows you to display targeted feedback to the user depending on what went wrong. + +#### Creating an `ActionError` + +To throw an error, import the `ActionError()` class from the `astro:actions` module. Pass it a human-readable status `code` (e.g. `"NOT_FOUND"` or `"BAD_REQUEST"`), and an optional `message` to provide further information about the error. + +This example throws an error from a `likePost` action when a user is not logged in, after checking a hypothetical "user-session" cookie for authentication: + +```ts title="src/actions/index.ts" ins=/ActionError(?= )/ ins={9-12} +import { defineAction, ActionError } from "astro:actions"; +import { z } from "astro:schema"; + +export const server = { + likePost: defineAction({ + input: z.object({ postId: z.string() }), + handler: async (input, ctx) => { + if (!ctx.cookies.has('user-session')) { + throw new ActionError({ + code: "UNAUTHORIZED", + message: "User must be logged in.", + }); + } + // Otherwise, like the post + }, + }), +}; +``` + +#### Handling an `ActionError` + +To handle this error, you can call the action from your application and check whether an `error` property is present. This property will be of type `ActionError` and will contain your `code` and `message`. + +In the following example, a `LikeButton.tsx` component calls the `likePost()` action when clicked. If an authentication error occurs, the `error.code` attribute is used to determine whether to display a login link: + +```tsx title=src/components/LikeButton.tsx ins="if (error.code === 'UNAUTHORIZED') setShowLogin(true);" +import { actions } from 'astro:actions'; +import { useState } from 'preact/hooks'; + +export function LikeButton({ postId }: { postId: string }) { + const [showLogin, setShowLogin] = useState(false); + return ( + <> + { + showLogin && Log in to like a post. + } + + + ) +} +``` + +### Handling client redirects + +When calling actions from the client, you can integrate with a client-side library like `react-router`, or you can use Astro's [`navigate()` function](/en/guides/view-transitions/#trigger-navigation) to redirect to a new page when an action succeeds. + +This example navigates to the homepage after a `logout` action returns successfully: + +```tsx title=src/pages/LogoutButton.tsx {2,7-8} +import { actions } from 'astro:actions'; +import { navigate } from 'astro:transitions/client'; + +export function LogoutButton() { + return ( + + ); +} +``` + +## Accepting form data from an action + +Actions accept JSON data by default. To accept form data from an HTML form, set `accept: 'form'` in your `defineAction()` call: + +```ts title="src/actions/index.ts" ins={6} +import { defineAction } from 'astro:actions'; +import { z } from 'astro:schema'; + +export const server = { + comment: defineAction({ + accept: 'form', + input: z.object(/* ... */), + handler: async (input) => { /* ... */ }, + }) +} +``` + +### Validating form data + +Actions will parse submitted form data to an object, using the value of each input’s `name` attribute as the object keys. For example, a form containing `` will be parsed to an object like `{ search: 'user input' }`. Your action's `input` schema will be used to validate this object. + +To receive the raw `FormData` object in your action handler instead of a parsed object, omit the `input` property in your action definition. + +The following example shows a validated newsletter registration form that accepts a user's email and requires a "terms of service" agreement checkbox. + + + +1. Create an HTML form component with unique `name` attributes on each input: + + ```astro title="src/components/Newsletter.astro" /name="\w+"/ +
+ + + + +
+ ``` + +2. Define a `newsletter` action to handle the submitted form. Validate the `email` field using the `z.string().email()` validator, and the `terms` checkbox using `z.boolean()`: + + ```ts title="src/actions/index.ts" ins={5-12} + import { defineAction } from 'astro:actions'; + import { z } from 'astro:schema'; + + export const server = { + newsletter: defineAction({ + accept: 'form', + input: z.object({ + email: z.string().email(), + terms: z.boolean(), + }), + handler: async ({ email, terms }) => { /* ... */ }, + }) + } + ``` + + See the [`input` API reference](/en/reference/api-reference/#input-validator) for all available form validators. + +3. Add a ` + ``` + + See [“Call actions from an HTML form action”](#call-actions-from-an-html-form-action) for an alternative way to submit form data. + +
+ +### Displaying form input errors + +You can validate form inputs before submission using [native HTML form validation attributes](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation#using_built-in_form_validation) like `required`, `type="email"`, and `pattern`. For more complex `input` validation on the backend, you can use the provided [`isInputError()`](/en/reference/api-reference/#isinputerror) utility function. + +To retrieve input errors, use the `isInputError()` utility to check whether an error was caused by invalid input. Input errors contain a `fields` object with messages for each input name that failed to validate. You can use these messages to prompt your user to correct their submission. + +The following example checks the error with `isInputError()`, then checks whether the error is in the email field, before finally creating a message from the errors. You can use JavaScript DOM manipulation or your preferred UI framework to display this message to users. + +```js /isInputError(?= )/ {5-12} +import { actions, isInputError } from 'astro:actions'; + +const form = document.querySelector('form'); +const formData = new FormData(form); +const { error } = await actions.newsletter(formData); +if (isInputError(error)) { + // Handle input errors. + if (error.fields.email) { + const message = error.fields.email.join(', '); + } +} +``` + +## Call actions from an HTML form action + +:::note +Pages must be on-demand rendered when calling actions using a form action. [Ensure prerendering is disabled on the page](/en/guides/server-side-rendering/#opting-out-of-pre-rendering-in-hybrid-mode) before using this API. +::: + +You can enable zero-JS form submissions with standard attributes on any `
` element. Form submissions without client-side JavaScript may be useful both as a fallback for when JavaScript fails to load, or if you prefer to handle forms entirely from the server. + +Calling [Astro.getActionResult()](/en/reference/api-reference/#astrogetactionresult) on the server returns the result of your form submission (`data` or `error`), and can be used to dynamically redirect, handle form errors, update the UI, and more. + +To call an action from an HTML form, add `method="POST"` to your ``, then set the form's `action` attribute using your action, for example `action={actions.logout}`. This will set the `action` attribute to use a query string that is handled by the server automatically. + +For example, this Astro component calls the `logout` action when the button is clicked and reloads the current page: + +```astro title="src/components/LogoutButton.astro" +--- +import { actions } from 'astro:actions'; +--- + + + +
+``` + +### Redirect on action success + +To navigate to a different page when an action is successful without client-side JavaScript, you can prepend a path in the `action` attribute. + +For example, `action={'/confirmation' + actions.newsletter}` will navigate to `/confirmation` when the `newsletter` action succeeds: + +```astro title="src/components/NewsletterSignup.astro" /action=\{[^\{\}]+\}/ +--- +import { actions } from 'astro:actions'; +--- + +
+ + +
+``` + +#### Dynamic redirect on action success + +If you need to decide where to redirect to dynamically, you can use an action’s result on the server. A common example is creating a product record and redirecting to the new product's page, e.g. `/products/[id]`. + +For example, say you have a `createProduct` action that returns the generated product id: + +```ts title="src/actions/index.ts" mark={10} +import { defineAction } from 'astro:actions'; +import { z } from 'astro:schema'; + +export const server = { + createProduct: defineAction({ + accept: 'form', + input: z.object({ /* ... */ }), + handler: async (input) => { + const product = await persistToDatabase(input); + return { id: product.id }; + }, + }) +} +``` + +You can retrieve the action result from your Astro component by calling `Astro.getActionResult()`. This returns an object containing `data` or `error` properties when an action is called, or `undefined` if the action was not called during this request. + +Use the `data` property to construct a URL to use with `Astro.redirect()`: + +```astro title="src/pages/products/create.astro" {4-7} +--- +import { actions } from 'astro:actions'; + +const result = Astro.getActionResult(actions.createProduct); +if (result && !result.error) { + return Astro.redirect(`/products/${result.data.id}`); +} +--- + +
+ +
+``` + +### Handle form action errors + +Astro will not redirect to your `action` route when an action fails. Instead, the current page is reloaded with any errors the action returned. Calling `Astro.getActionResult()` in the Astro component containing your form gives you access to the `error` object for custom error handling. + +The following example displays a general failure message when a `newsletter` action fails: + +```astro title="src/pages/index.astro" {4,7-9} +--- +import { actions } from 'astro:actions'; + +const result = Astro.getActionResult(actions.newsletter); +--- + +{result?.error && ( +

Unable to sign up. Please try again later.

+)} +
+ + +
+``` + +For more customization, you can [use the `isInputError()` utility](#displaying-form-input-errors) to check whether an error is caused by invalid input. + +The following example renders an error banner under the `email` input field when an invalid email is submitted: + +```astro title="src/pages/index.astro" ins={5,13} ins='aria-describedby="error"' +--- +import { actions, isInputError } from 'astro:actions'; + +const result = Astro.getActionResult(actions.newsletter); +const inputErrors = isInputError(result?.error) ? result.error.fields : {}; +--- + +
+ + {inputErrors.email &&

{inputErrors.email.join(',')}

} + +
+``` + +:::note +Astro persists action `data` and `error` with a single-use cookie. This means `getActionResult()` will return a result on the first request _only_, and `undefined` when revisiting the page. +::: + +#### Preserve input values on error + +Inputs will be cleared whenever a form is submitted. To persist input values, you can [enable view transitions](/en/guides/view-transitions/#adding-view-transitions-to-a-page) on the page and apply the `transition:persist` directive to each input: + +```astro ins="transition:persist" + +``` + +### Update the UI with a form action result + +The result returned by `Astro.getActionResult()` is single-use, and will reset to `undefined` whenever the page is refreshed. This is ideal for [displaying input errors](#handle-form-action-errors) and showing temporary notifications to the user on success. + +:::tip +If you need a result to be displayed across page refreshes, consider storing the result in a database or [in a cookie](/en/reference/api-reference/#astrocookies). +::: + +Pass an action to `Astro.getActionResult()` and use the returned `data` property to render any temporary UI you want to display. This example uses the `productName` property returned by an `addToCart` action to show a success message: + +```astro title="src/pages/products/[slug].astro" +--- +import { actions } from 'astro:actions'; + +const result = Astro.getActionResult(actions.addToCart); +--- + +{result && !result.error && ( +

Added {result.data.productName} to cart

+)} + + +``` + +:::caution +Action data is passed using a persisted cookie. **This cookie is not encrypted.** In general, we recommend returning the minimum information required from your action `handler` to avoid vulnerabilities, and persist other sensitive information in a database. + +For example, you might return the name of a product in an `addToCart` action, rather than returning the entire `product` object: + +```ts title="src/actions/index.ts" del={7} ins={8} +import { defineAction } from 'astro:actions'; + +export const server = { + addToCard: defineAction({ + handler: async () => { + /* ... */ + return product; + return { productName: product.name }; + } + }) +} +``` +::: diff --git a/src/content/docs/en/reference/api-reference.mdx b/src/content/docs/en/reference/api-reference.mdx index 35dcff09e3cfd..9513bdf326bd3 100644 --- a/src/content/docs/en/reference/api-reference.mdx +++ b/src/content/docs/en/reference/api-reference.mdx @@ -764,6 +764,44 @@ This property is only available when building for SSR (server-side rendering) an The locale computed from the current URL, using the syntax specified in your `locales` configuration. If the URL does not contain a `/[locale]/` prefix, then the value will default to `i18n.defaultLocale`. +### `Astro.getActionResult()` + +

+**Type:** `(action: TAction) => ActionReturnType | undefined`
+ +

+ +`Astro.getActionResult()` is a function that returns the result of an [Action](/en/guides/actions/) submission. This accepts an action function as an argument (e.g. `actions.logout`) and returns a `data` or `error` object when a submission is received. Otherwise, it will return `undefined`. + +```astro title="src/pages/index.astro" +--- +import { actions } from 'astro:actions'; + +const result = Astro.getActionResult(actions.logout); +--- + +
+ +
+{result?.error &&

Failed to log out. Please try again.

} +``` + +### `Astro.callAction()` + +

+ +

+ +`Astro.callAction()` is a function used to call an Action handler directly from your Astro component. This function accepts an Action function as the first argument (e.g. `actions.logout`) and any input that action receives as the second argument. It returns the result of the action as a promise. + +```astro title="src/pages/index.astro" +--- +import { actions } from 'astro:actions'; + +const { data, error } = await Astro.callAction(actions.logout, { userId: '123' }); +--- +``` + ## Endpoint Context [Endpoint functions](/en/guides/endpoints/) receive a context object as the first parameter. It mirrors many of the `Astro` global properties. @@ -1028,6 +1066,42 @@ export function GET({ locals }: APIContext) { See also: [`Astro.locals`](#astrolocals) +### `context.getActionResult()` + +

+ +**Type:** `(action: TAction) => ActionReturnType | undefined`
+ +

+ +`context.getActionResult()` is a function that returns the result of an [Action](/en/guides/actions/) submission. This accepts an action function as an argument (e.g. `actions.logout`), and returns a `data` or `error` object when a submission is received. Otherwise, it will return `undefined`. + + +See also [`Astro.getActionResult()`](#astrogetactionresult) + +### `context.callAction()` + +

+ +

+ +`context.callAction()` is a function used to call an Action handler directly from your Astro component. This function accepts an Action function as the first argument (e.g. `actions.logout`) and any input that action receives as the second argument. It returns the result of the action as a promise. + + +See also [`Astro.callAction()`](#astrocallaction) + +## Endpoint Context + +[Endpoint functions](/en/guides/endpoints/) receive a context object as the first parameter. It mirrors many of the `Astro` global properties. + +```ts title="endpoint.json.ts" +import type { APIContext } from 'astro'; + +export function GET(context: APIContext) { + // ... +} +``` + ## `getStaticPaths()` **Type:** `(options: GetStaticPathsOptions) => Promise | GetStaticPathsResult` @@ -2075,6 +2149,118 @@ export const onRequest = defineMiddleware(async (context, next) => { }) ``` +## Actions (astro:actions) + +

+ +

+ +Actions are backend functions used for type-safe server-to-client communication. All utilities to define and call actions are exposed by the `astro:actions` module. For examples and usage instructions, [see the Actions guide](/en/guides/actions/). + +### `defineAction()` + +

+ +

+ +The `defineAction()` utility is used to define new actions from the `src/actions/index.ts` file. This accepts a `handler()` function containing the server logic to run, and an optional `input` property to validate input parameters at runtime. + +```ts +export const server = { + getGreeting: defineAction({ + input: z.object({ + name: z.string(), + }), + handler: async (input) => { + return `Hello, ${input.name}!` + } + }) +} +``` + +#### `handler()` property + +

+ +

+ +`defineAction()` accepts a `handler()` function containing the server logic to run when the action is called. This function can return data that is automatically serialized and sent to the caller. + +Return values are parsed using the [devalue library](https://github.com/Rich-Harris/devalue). This supports JSON values, along with instances of `Date()`, `Map()`, `Set()`, or `URL()`. + +#### `input` validator + +

+ +

+ +The optional `input` property accepts a Zod validator to validate handler inputs at runtime. If the action fails to validate, [a `BAD_REQUEST` error](#actionerror) is returned and the `handler` is not called. + +If used with `accept: 'form'`, `input` must use the `z.object()` validator. + +Extension functions including `.refine()`, `.transform()`, and `.pipe()` are also supported on this object. The following validators are supported for form data fields: + +- Inputs of type `number` can be validated using `z.number()` +- Inputs of type `checkbox` can be validated using `z.boolean()` +- Inputs of type `file` can be validated using `z.instanceof(File)` +- Multiple inputs of the same `name` can be validated using `z.array(/* validator */)` +- All other inputs can be validated using `z.string()` + +### `isInputError()` + +

+ +**Type:** (error?: unknown | ActionError) => boolean
+ +

+ +The `isInputError()` utility is used to check whether an `ActionError` is an input validation error. When the `input` validator is a `z.object()`, input errors include a `fields` object with error messages grouped by name. + +See the [form input errors guide](/en/guides/actions/#displaying-form-input-errors) for more on using `isInputError()`. + +### `ActionError` + +

+ +

+ +The `ActionError()` constructor is used to create errors thrown by an action `handler`. This accepts a `code` property describing the error that occurred (example: `"UNAUTHORIZED"`), and an optional `message` property with further details. + +#### `code` + +

+ +

+ +The `code` property accepts human-readable versions of all HTTP status codes. The following codes are supported: + +- `BAD_REQUEST` (400): The client sent invalid input. This error is thrown when an action `input` validator fails to validate. +- `UNAUTHORIZED` (401): The client lacks valid authentication credentials. +- `FORBIDDEN` (403): The client is not authorized to access a resource. +- `NOT_FOUND` (404): The server cannot find the requested resource. +- `METHOD_NOT_SUPPORTED` (405): The server does not support the requested method. +- `TIMEOUT` (408): The server timed out while processing the request. +- `CONFLICT` (409): The server cannot update a resource due to a conflict. +- `PRECONDITION_FAILED` (412): The server does not meet a precondition of the request. +- `PAYLOAD_TOO_LARGE` (413): The server cannot process the request because the payload is too large. +- `UNSUPPORTED_MEDIA_TYPE` (415): The server does not support the request's media type. Note: Actions already check [the `Content-Type` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) for JSON and form requests, so you likely won't need to raise this code manually. +- `UNPROCESSABLE_CONTENT` (422): The server cannot process the request due to semantic errors. +- `TOO_MANY_REQUESTS` (429): The server has exceeded a specified rate limit. +- `CLIENT_CLOSED_REQUEST` (499): The client closed the request before the server could respond. +- `INTERNAL_SERVER_ERROR` (500): The server failed unexpectedly. +- `NOT_IMPLEMENTED` (501): The server does not support the requested feature. +- `BAD_GATEWAY` (502): The server received an invalid response from an upstream server. +- `SERVICE_UNAVAILABLE` (503): The server is temporarily unavailable. +- `GATEWAY_TIMEOUT` (504): The server received a timeout from an upstream server. + +#### `message` + +

+ +

+ +The `message` property accepts a string. (e.g. "User must be logged in.") + ## Built-in Components Astro includes several built-in components for you to use in your projects. All built-in components are available in `.astro` files via `import {} from 'astro:components';`. @@ -2332,4 +2518,4 @@ const serverObject = { This component provides a way to inspect values on the client-side, without any JavaScript. -[canonical]: https://en.wikipedia.org/wiki/Canonical_link_element +[canonical]: https://en.wikipedia.org/wiki/Canonical_link_element \ No newline at end of file diff --git a/src/i18n/en/nav.ts b/src/i18n/en/nav.ts index a9a330d69dd3b..b938317e91bac 100644 --- a/src/i18n/en/nav.ts +++ b/src/i18n/en/nav.ts @@ -53,6 +53,7 @@ export default [ { text: 'Routes and Navigation', header: true, type: 'learn', key: 'routes' }, { text: 'Routing', slug: 'guides/routing', key: 'guides/routing' }, { text: 'Endpoints', slug: 'guides/endpoints', key: 'guides/endpoints' }, + { text: 'Actions', slug: 'guides/actions', key: 'guides/actions' }, { text: 'Prefetch', slug: 'guides/prefetch', key: 'guides/prefetch' }, { text: 'Middleware', slug: 'guides/middleware', key: 'guides/middleware' }, {