Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: withastro/astro
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: astro@4.8.7
Choose a base ref
...
head repository: withastro/astro
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: astro@4.9.0
Choose a head ref

Commits on May 22, 2024

  1. chore(db): move tests to node runner (#11109)

    ematipico authored May 22, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    IvanGoncharov Ivan Goncharov
    Copy the full SHA
    05ef10c View commit details
  2. [ci] format

    ematipico authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    e1884ea View commit details
  3. chore: fix missed failing test (#11115)

    ematipico authored May 22, 2024
    Copy the full SHA
    6988f1d View commit details
  4. [ci] format

    ematipico authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    a3675e5 View commit details
  5. feat: make i18n domains stable (#11022)

    * feat: make i18n domains stable
    
    * update tst
    
    * Update .changeset/five-crabs-rhyme.md
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    * fix regression
    
    ---------
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    ematipico and sarah11918 authored May 22, 2024
    Copy the full SHA
    be68ab4 View commit details
  6. [ci] format

    ematipico authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    c30a415 View commit details
  7. feat: make CSRF protection stable (#11021)

    * feat: make CSRF protection stable
    
    * revert change
    
    * Apply suggestions from code review
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    * Update packages/astro/src/@types/astro.ts
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    * Update packages/astro/src/@types/astro.ts
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    * beef up changeset
    
    * Update .changeset/chatty-experts-smell.md
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    * Update .changeset/chatty-experts-smell.md
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    * move section
    
    * Apply suggestions from code review
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    ---------
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    ematipico and sarah11918 authored May 22, 2024
    Copy the full SHA
    2d4c8fa View commit details
  8. feat: prefer using x-forwarded-for as clientAddress (#11101)

    * feat: change node clientAddress use x-forwarded-for
    
    when
    
    ```
    adapter: node({
        mode: 'standalone',
      })
    ```
    
    * feat: prefer using x-forwarded-for as clientAddress
    
    * Update .changeset/healthy-planets-dream.md
    
    Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
    
    * Update .changeset/healthy-planets-dream.md
    
    Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
    
    * Apply suggestions from code review
    
    ---------
    
    Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
    linguofeng and ematipico authored May 22, 2024
    Copy the full SHA
    a6916e4 View commit details
  9. feat: container APIs (#11051)

    * feat: container APIs
    
    * chore: handle runtime mode
    
    * chore: render slots
    
    * more prototyping
    
    * Adding a changeset
    
    * fix some weirdness around types
    
    * feat: allow to inject the manifest
    
    * feat: accept a manifest
    
    * more APIs
    
    * add `route` to the options
    
    * chore
    
    * fix component instance
    
    * chore: document stuff
    
    * remove commented code
    
    * chore: add test for renderers and fixed its types
    
    * fix: update name of the example
    
    * fix regression inside tests
    
    * use `experimental_`
    
    * remove errors
    
    * need to understand the types here
    
    * remove some options that I don't deem necessary for this phase
    
    * remove superfluous comments
    
    * chore: remove useless `@ts-ignore` directive
    
    * chore: address feedback
    
    * fix regression and remove astro config
    
    * chore: fix regression
    
    * Apply suggestions from code review
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    * ooops
    
    * restore changes
    
    ---------
    
    Co-authored-by: Matthew Phillips <matthew@skypack.dev>
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    3 people authored May 22, 2024
    Copy the full SHA
    12a1bcc View commit details
  10. [ci] format

    ematipico authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    b15949f View commit details
  11. Actions: restore api context param (#11112)

    * feat: expose APIContext from the second handler param
    
    * refactor: use second param from test
    
    * chore: changeset
    
    * edit: minor -> patch
    
    * edit: apiContext -> context
    
    Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
    
    * refactor: apiContext -> context
    
    Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
    
    * refactor: apiContext -> context
    
    Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
    
    ---------
    
    Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
    bholmesdev and florian-lefebvre authored May 22, 2024
    Copy the full SHA
    29a8650 View commit details
  12. [ci] format

    bholmesdev authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    a687a17 View commit details
  13. feat(vue): add support vue devtools options (#11055)

    Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>
    Co-authored-by: Jan-Niklas Wortmann <jan-niklas.wortmann@jetbrains.com>
    Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
    4 people authored May 22, 2024
    Copy the full SHA
    b92de22 View commit details
  14. [ci] format

    florian-lefebvre authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    3dd57f6 View commit details
  15. Actions: React 19 progressive enhancement support (#11071)

    * deps: react 19
    
    * feat: react progressive enhancement with useActionState
    
    * refactor: revert old action state implementation
    
    * feat(test): react 19 action with useFormStatus
    
    * fix: remove unused context arg
    
    * fix: wrote actions to wrong test fixture!
    
    * deps: revert react 19 beta to 18 for actions-blog fixture
    
    * chore: remove unused overrides
    
    * chore: remove unused actions export
    
    * chore: spaces vs tabs ugh
    
    * chore: fix conflicting fixture names
    
    * chore: changeset
    
    * chore: bump changeset to minor
    
    * Actions: support React 19 `useActionState()` with progressive enhancement (#11074)
    
    * feat(ex): Like with useActionState
    
    * feat: useActionState progressive enhancement!
    
    * feat: getActionState utility
    
    * chore: revert actions-blog fixture experimentation
    
    * fix: add back actions.ts export
    
    * feat(test): Like with use action state test
    
    * fix: stub form state client-side to avoid hydration error
    
    * fix: bad .safe chaining
    
    * fix: update actionState for client call
    
    * fix: correctly resume form state client side
    
    * refactor: unify and document reactServerActionResult
    
    * feat(test): useActionState assertions
    
    * feat(docs): explain my mess
    
    * refactor: add experimental_ prefix
    
    * refactor: move all react internals to integration
    
    * chore: remove unused getIslandProps
    
    * chore: remove unused imports
    
    * chore: undo format changes
    
    * refactor: get actionResult from middleware directly
    
    * refactor: remove bad result type
    
    * fix: like button disabled timeout
    
    * chore: changeset
    
    * refactor: remove request cloning
    
    * Update .changeset/gentle-windows-enjoy.md
    
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    
    * changeset grammar tense
    
    ---------
    
    Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
    3 people authored May 22, 2024
    Copy the full SHA
    8ca7c73 View commit details
  16. [ci] format

    ematipico authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    7be7b1a View commit details
  17. [docs] updates security.checkOrigin config reference (#11117)

    sarah11918 authored May 22, 2024
    Copy the full SHA
    5cb68d8 View commit details
  18. [ci] format

    ematipico authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    ae42bf3 View commit details
  19. [docs] update heading level in config reference (#11118)

    sarah11918 authored May 22, 2024
    Copy the full SHA
    c7386ef View commit details
  20. [ci] format

    sarah11918 authored and astrobot-houston committed May 22, 2024
    Copy the full SHA
    7bfe8aa View commit details
  21. [docs] fix config reference code formatting (#11119)

    sarah11918 authored May 22, 2024
    Copy the full SHA
    e71348e View commit details
  22. Actions: Allow actions to be called on the server (#11088)

    * wip: consume async local storage from `defineAction()`
    
    * fix: move async local storage to middleware. It works!
    
    * refactor: remove content-type check on JSON. Not needed
    
    * chore: remove test
    
    * feat: support server action calls
    
    * refactor: parse path keys within getAction
    
    * feat(test): server-side action call
    
    * chore: changeset
    
    * fix: reapply context on detected rewrite
    
    * feat(test): action from server with rewrite
    
    * chore: stray import change
    
    * feat(docs): add endpoints to changeset
    
    * chore: minor -> patch
    
    * fix: move rewrite check to start of middleware
    
    * fix: bad getApiContext() import
    
    ---------
    
    Co-authored-by: bholmesdev <bholmesdev@gmail.com>
    bholmesdev and bholmesdev authored May 22, 2024
    Copy the full SHA
    9566fa0 View commit details

Commits on May 23, 2024

  1. Let web vitals route handle all requests under that path (#11120)

    delucis authored May 23, 2024
    Copy the full SHA
    9a0e94b View commit details
  2. [ci] release (#11116)

    Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
    astrobot-houston and github-actions[bot] authored May 23, 2024
    Copy the full SHA
    5077592 View commit details
Showing with 2,903 additions and 718 deletions.
  1. +1 −1 examples/basics/package.json
  2. +1 −1 examples/blog/package.json
  3. +1 −1 examples/component/package.json
  4. +1 −0 examples/container-with-vitest/.codesandbox/Dockerfile
  5. +24 −0 examples/container-with-vitest/.gitignore
  6. +11 −0 examples/container-with-vitest/README.md
  7. +7 −0 examples/container-with-vitest/astro.config.ts
  8. +25 −0 examples/container-with-vitest/package.json
  9. +9 −0 examples/container-with-vitest/public/favicon.svg
  10. +8 −0 examples/container-with-vitest/src/components/Card.astro
  11. +14 −0 examples/container-with-vitest/src/components/Counter.jsx
  12. +5 −0 examples/container-with-vitest/src/components/ReactWrapper.astro
  13. +20 −0 examples/container-with-vitest/src/pages/[locale].astro
  14. +11 −0 examples/container-with-vitest/src/pages/api.ts
  15. +16 −0 examples/container-with-vitest/src/pages/index.astro
  16. +15 −0 examples/container-with-vitest/test/Card.test.ts
  17. +19 −0 examples/container-with-vitest/test/ReactWrapper.test.ts
  18. +16 −0 examples/container-with-vitest/test/[locale].test.ts
  19. +3 −0 examples/container-with-vitest/tsconfig.json
  20. +9 −0 examples/container-with-vitest/vitest.config.ts
  21. +1 −1 examples/framework-alpine/package.json
  22. +1 −1 examples/framework-lit/package.json
  23. +3 −3 examples/framework-multiple/package.json
  24. +1 −1 examples/framework-preact/package.json
  25. +2 −2 examples/framework-react/package.json
  26. +1 −1 examples/framework-solid/package.json
  27. +1 −1 examples/framework-svelte/package.json
  28. +2 −2 examples/framework-vue/package.json
  29. +1 −1 examples/hackernews/package.json
  30. +1 −1 examples/integration/package.json
  31. +1 −1 examples/middleware/package.json
  32. +1 −1 examples/minimal/package.json
  33. +1 −1 examples/non-html-pages/package.json
  34. +1 −1 examples/portfolio/package.json
  35. +1 −1 examples/ssr/package.json
  36. +1 −1 examples/starlog/package.json
  37. +1 −1 examples/toolbar-app/package.json
  38. +1 −1 examples/view-transitions/package.json
  39. +1 −1 examples/with-markdoc/package.json
  40. +1 −1 examples/with-markdown-plugins/package.json
  41. +1 −1 examples/with-markdown-shiki/package.json
  42. +1 −1 examples/with-mdx/package.json
  43. +1 −1 examples/with-nanostores/package.json
  44. +1 −1 examples/with-tailwindcss/package.json
  45. +1 −1 examples/with-vitest/package.json
  46. +181 −0 packages/astro/CHANGELOG.md
  47. +16 −0 packages/astro/e2e/actions-blog.test.js
  48. +61 −0 packages/astro/e2e/actions-react-19.test.js
  49. +1 −1 packages/astro/e2e/fixtures/actions-blog/package.json
  50. +1 −1 packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts
  51. +10 −0 packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro
  52. +17 −0 packages/astro/e2e/fixtures/actions-react-19/astro.config.mjs
  53. +21 −0 packages/astro/e2e/fixtures/actions-react-19/db/config.ts
  54. +15 −0 packages/astro/e2e/fixtures/actions-react-19/db/seed.ts
  55. +28 −0 packages/astro/e2e/fixtures/actions-react-19/package.json
  56. +47 −0 packages/astro/e2e/fixtures/actions-react-19/src/actions/index.ts
  57. +43 −0 packages/astro/e2e/fixtures/actions-react-19/src/components/BaseHead.astro
  58. +62 −0 packages/astro/e2e/fixtures/actions-react-19/src/components/Footer.astro
  59. +17 −0 packages/astro/e2e/fixtures/actions-react-19/src/components/FormattedDate.astro
  60. +83 −0 packages/astro/e2e/fixtures/actions-react-19/src/components/Header.astro
  61. +25 −0 packages/astro/e2e/fixtures/actions-react-19/src/components/HeaderLink.astro
  62. +38 −0 packages/astro/e2e/fixtures/actions-react-19/src/components/Like.tsx
  63. +5 −0 packages/astro/e2e/fixtures/actions-react-19/src/consts.ts
  64. +15 −0 packages/astro/e2e/fixtures/actions-react-19/src/content/blog/first-post.md
  65. +16 −0 packages/astro/e2e/fixtures/actions-react-19/src/content/config.ts
  66. +85 −0 packages/astro/e2e/fixtures/actions-react-19/src/layouts/BlogPost.astro
  67. +40 −0 packages/astro/e2e/fixtures/actions-react-19/src/pages/blog/[...slug].astro
  68. +111 −0 packages/astro/e2e/fixtures/actions-react-19/src/pages/blog/index.astro
  69. +140 −0 packages/astro/e2e/fixtures/actions-react-19/src/styles/global.css
  70. +8 −0 packages/astro/e2e/fixtures/actions-react-19/tsconfig.json
  71. +5 −1 packages/astro/package.json
  72. +45 −99 packages/astro/src/@types/astro.ts
  73. +31 −29 packages/astro/src/actions/runtime/middleware.ts
  74. +1 −2 packages/astro/src/actions/runtime/route.ts
  75. +7 −1 packages/astro/src/actions/runtime/utils.ts
  76. +11 −12 packages/astro/src/actions/runtime/virtual/server.ts
  77. +1 −1 packages/astro/src/actions/utils.ts
  78. +424 −0 packages/astro/src/container/index.ts
  79. +114 −0 packages/astro/src/container/pipeline.ts
  80. +5 −1 packages/astro/src/core/app/node.ts
  81. +4 −4 packages/astro/src/core/build/generate.ts
  82. +2 −2 packages/astro/src/core/build/plugins/plugin-manifest.ts
  83. +22 −31 packages/astro/src/core/config/schema.ts
  84. +12 −3 packages/astro/src/core/render-context.ts
  85. +9 −10 packages/astro/src/core/routing/manifest/create.ts
  86. +1 −1 packages/astro/src/integrations/features-validation.ts
  87. +1 −2 packages/astro/src/vite-plugin-astro-server/plugin.ts
  88. +37 −12 packages/astro/templates/actions.mjs
  89. +11 −0 packages/astro/test/actions.test.js
  90. +27 −0 packages/astro/test/client-address-node.test.js
  91. +142 −0 packages/astro/test/container.test.js
  92. +14 −5 packages/astro/test/fixtures/actions/src/actions/index.ts
  93. +3 −0 packages/astro/test/fixtures/actions/src/pages/rewrite.astro
  94. +11 −0 packages/astro/test/fixtures/actions/src/pages/subscribe.astro
  95. +8 −0 packages/astro/test/fixtures/client-address-node/astro.config.mjs
  96. +9 −0 packages/astro/test/fixtures/client-address-node/package.json
  97. +13 −0 packages/astro/test/fixtures/client-address-node/src/pages/index.astro
  98. +2 −6 packages/astro/test/fixtures/csrf-check-origin/astro.config.mjs
  99. +0 −3 packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs
  100. +8 −0 packages/astro/test/test-utils.js
  101. +0 −6 packages/astro/test/units/config/config-validate.test.js
  102. +13 −14 packages/astro/test/units/i18n/astro_i18n.test.js
  103. +1 −5 packages/db/package.json
  104. +21 −20 packages/db/test/basics.test.js
  105. +4 −3 packages/db/test/db-in-src.test.js
  106. +4 −3 packages/db/test/error-handling.test.js
  107. +6 −5 packages/db/test/integration-only.test.js
  108. +10 −9 packages/db/test/integrations.test.js
  109. +6 −5 packages/db/test/local-prod.test.js
  110. +6 −5 packages/db/test/no-seed.test.js
  111. +4 −3 packages/db/test/ssr-no-apptoken.test.js
  112. +4 −3 packages/db/test/static-remote.test.js
  113. +45 −44 packages/db/test/unit/column-queries.test.js
  114. +11 −11 packages/db/test/unit/index-queries.test.js
  115. +16 −15 packages/db/test/unit/reference-queries.test.js
  116. +4 −4 packages/db/test/unit/reset-queries.test.js
  117. +47 −0 packages/integrations/react/CHANGELOG.md
  118. +11 −0 packages/integrations/react/client.js
  119. +2 −1 packages/integrations/react/package.json
  120. +55 −0 packages/integrations/react/server.js
  121. +101 −0 packages/integrations/react/src/actions.ts
  122. +22 −0 packages/integrations/vue/CHANGELOG.md
  123. +2 −2 packages/integrations/vue/package.json
  124. +4 −1 packages/integrations/vue/src/index.ts
  125. +6 −0 packages/integrations/web-vitals/CHANGELOG.md
  126. +1 −1 packages/integrations/web-vitals/package.json
  127. +1 −1 packages/integrations/web-vitals/src/index.ts
  128. +185 −298 pnpm-lock.yaml
2 changes: 1 addition & 1 deletion examples/basics/package.json
Original file line number Diff line number Diff line change
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/blog/package.json
Original file line number Diff line number Diff line change
@@ -14,6 +14,6 @@
"@astrojs/mdx": "^3.0.1",
"@astrojs/rss": "^4.0.6",
"@astrojs/sitemap": "^3.1.5",
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/component/package.json
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
"astro": "^4.8.7"
"astro": "^4.9.0"
},
"peerDependencies": {
"astro": "^4.0.0"
1 change: 1 addition & 0 deletions examples/container-with-vitest/.codesandbox/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM node:18-bullseye
24 changes: 24 additions & 0 deletions examples/container-with-vitest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/

# dependencies
node_modules/

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*


# environment variables
.env
.env.production

# macOS-specific files
.DS_Store

# jetbrains setting folder
.idea/
11 changes: 11 additions & 0 deletions examples/container-with-vitest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Astro + [Vitest](https://vitest.dev/) + Container API Example

```sh
npm create astro@latest -- --template container-with-vitest
```

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-vitest)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-vitest)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-vitest/devcontainer.json)

This example showcases Astro working with [Vitest](https://vitest.dev/) and how to test components using the Container API.
7 changes: 7 additions & 0 deletions examples/container-with-vitest/astro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'astro/config';
import react from "@astrojs/react"

// https://astro.build/config
export default defineConfig({
integrations: [react()]
});
25 changes: 25 additions & 0 deletions examples/container-with-vitest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@example/container-with-vitest",
"type": "module",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"test": "vitest run"
},
"dependencies": {
"astro": "^4.9.0",
"@astrojs/react": "^3.4.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"vitest": "^1.6.0"
},
"devDependencies": {
"@types/react-dom": "^18.3.0",
"@types/react": "^18.3.2"
}
}
9 changes: 9 additions & 0 deletions examples/container-with-vitest/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions examples/container-with-vitest/src/components/Card.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
---

<div>
This is a card
<slot />
</div>
14 changes: 14 additions & 0 deletions examples/container-with-vitest/src/components/Counter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState } from 'react';

export default function({ initialCount }) {
const [count, setCount] = useState(initialCount || 0);
return (
<div className="rounded-t-lg overflow-hidden border-t border-l border-r border-gray-400 text-center p-4">
<h2 className="font-semibold text-lg">Counter</h2>
<h3 className="font-medium text-lg">Count: {count}</h3>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
import Counter from './Counter.jsx';
---

<Counter initialCount={5} />
20 changes: 20 additions & 0 deletions examples/container-with-vitest/src/pages/[locale].astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
export function getStaticPaths() {
return [{ params: { locale: 'en' } }];
}
const { locale } = Astro.params;
---

<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>Astro</h1>
<p>Locale: {locale}</p>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/container-with-vitest/src/pages/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function GET() {
const json = {
foo: 'bar',
number: 1,
};
return new Response(JSON.stringify(json), {
headers: {
'content-type': 'application/json',
},
});
}
16 changes: 16 additions & 0 deletions examples/container-with-vitest/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
---

<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>Astro</h1>
</body>
</html>
15 changes: 15 additions & 0 deletions examples/container-with-vitest/test/Card.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';

test('Card with slots', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Card, {
slots: {
default: 'Card content',
},
});

expect(result).toContain('This is a card');
expect(result).toContain('Card content');
});
19 changes: 19 additions & 0 deletions examples/container-with-vitest/test/ReactWrapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import ReactWrapper from '../src/components/ReactWrapper.astro';

test('ReactWrapper with react renderer', async () => {
const container = await AstroContainer.create({
renderers: [
{
name: '@astrojs/react',
clientEntrypoint: '@astrojs/react/client.js',
serverEntrypoint: '@astrojs/react/server.js',
},
],
});
const result = await container.renderToString(ReactWrapper);

expect(result).toContain('Counter');
expect(result).toContain('Count: <!-- -->5');
});
16 changes: 16 additions & 0 deletions examples/container-with-vitest/test/[locale].test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Locale from '../src/pages/[locale].astro';

test('Dynamic route', async () => {
const container = await AstroContainer.create();
// @ts-ignore
const result = await container.renderToString(Locale, {
params: {
locale: 'en',
},
request: new Request('http://example.com/en'),
});

expect(result).toContain('Locale: en');
});
3 changes: 3 additions & 0 deletions examples/container-with-vitest/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/base"
}
9 changes: 9 additions & 0 deletions examples/container-with-vitest/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// <reference types="vitest" />
import { getViteConfig } from 'astro/config';

export default getViteConfig({
test: {
/* for example, use global to avoid globals imports (describe, test, expect): */
// globals: true,
},
});
2 changes: 1 addition & 1 deletion examples/framework-alpine/package.json
Original file line number Diff line number Diff line change
@@ -14,6 +14,6 @@
"@astrojs/alpinejs": "^0.4.0",
"@types/alpinejs": "^3.13.10",
"alpinejs": "^3.13.10",
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/framework-lit/package.json
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/lit": "^4.0.1",
"@webcomponents/template-shadowroot": "^0.2.1",
"astro": "^4.8.7",
"astro": "^4.9.0",
"lit": "^3.1.3"
}
}
6 changes: 3 additions & 3 deletions examples/framework-multiple/package.json
Original file line number Diff line number Diff line change
@@ -12,13 +12,13 @@
},
"dependencies": {
"@astrojs/preact": "^3.3.0",
"@astrojs/react": "^3.3.4",
"@astrojs/react": "^3.4.0",
"@astrojs/solid-js": "^4.2.0",
"@astrojs/svelte": "^5.4.0",
"@astrojs/vue": "^4.2.0",
"@astrojs/vue": "^4.3.0",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"astro": "^4.8.7",
"astro": "^4.9.0",
"preact": "^10.21.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
2 changes: 1 addition & 1 deletion examples/framework-preact/package.json
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/preact": "^3.3.0",
"@preact/signals": "^1.2.3",
"astro": "^4.8.7",
"astro": "^4.9.0",
"preact": "^10.21.0"
}
}
4 changes: 2 additions & 2 deletions examples/framework-react/package.json
Original file line number Diff line number Diff line change
@@ -11,10 +11,10 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^3.3.4",
"@astrojs/react": "^3.4.0",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"astro": "^4.8.7",
"astro": "^4.9.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
2 changes: 1 addition & 1 deletion examples/framework-solid/package.json
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/solid-js": "^4.2.0",
"astro": "^4.8.7",
"astro": "^4.9.0",
"solid-js": "^1.8.17"
}
}
2 changes: 1 addition & 1 deletion examples/framework-svelte/package.json
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/svelte": "^5.4.0",
"astro": "^4.8.7",
"astro": "^4.9.0",
"svelte": "^4.2.16"
}
}
4 changes: 2 additions & 2 deletions examples/framework-vue/package.json
Original file line number Diff line number Diff line change
@@ -11,8 +11,8 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/vue": "^4.2.0",
"astro": "^4.8.7",
"@astrojs/vue": "^4.3.0",
"astro": "^4.9.0",
"vue": "^3.4.27"
}
}
2 changes: 1 addition & 1 deletion examples/hackernews/package.json
Original file line number Diff line number Diff line change
@@ -12,6 +12,6 @@
},
"dependencies": {
"@astrojs/node": "^8.2.5",
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/integration/package.json
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
"astro": "^4.8.7"
"astro": "^4.9.0"
},
"peerDependencies": {
"astro": "^4.0.0"
2 changes: 1 addition & 1 deletion examples/middleware/package.json
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
},
"dependencies": {
"@astrojs/node": "^8.2.5",
"astro": "^4.8.7",
"astro": "^4.9.0",
"html-minifier": "^4.0.0"
},
"devDependencies": {
2 changes: 1 addition & 1 deletion examples/minimal/package.json
Original file line number Diff line number Diff line change
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/non-html-pages/package.json
Original file line number Diff line number Diff line change
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/portfolio/package.json
Original file line number Diff line number Diff line change
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/ssr/package.json
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
"dependencies": {
"@astrojs/node": "^8.2.5",
"@astrojs/svelte": "^5.4.0",
"astro": "^4.8.7",
"astro": "^4.9.0",
"svelte": "^4.2.16"
}
}
2 changes: 1 addition & 1 deletion examples/starlog/package.json
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.8.7",
"astro": "^4.9.0",
"sass": "^1.77.1",
"sharp": "^0.33.3"
}
2 changes: 1 addition & 1 deletion examples/toolbar-app/package.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,6 @@
"./app": "./dist/app.js"
},
"devDependencies": {
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/view-transitions/package.json
Original file line number Diff line number Diff line change
@@ -12,6 +12,6 @@
"devDependencies": {
"@astrojs/tailwind": "^5.1.0",
"@astrojs/node": "^8.2.5",
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/with-markdoc/package.json
Original file line number Diff line number Diff line change
@@ -12,6 +12,6 @@
},
"dependencies": {
"@astrojs/markdoc": "^0.11.0",
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/with-markdown-plugins/package.json
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/markdown-remark": "^5.1.0",
"astro": "^4.8.7",
"astro": "^4.9.0",
"hast-util-select": "^6.0.2",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
2 changes: 1 addition & 1 deletion examples/with-markdown-shiki/package.json
Original file line number Diff line number Diff line change
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^4.8.7"
"astro": "^4.9.0"
}
}
2 changes: 1 addition & 1 deletion examples/with-mdx/package.json
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/mdx": "^3.0.1",
"@astrojs/preact": "^3.3.0",
"astro": "^4.8.7",
"astro": "^4.9.0",
"preact": "^10.21.0"
}
}
2 changes: 1 addition & 1 deletion examples/with-nanostores/package.json
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/preact": "^3.3.0",
"@nanostores/preact": "^0.5.1",
"astro": "^4.8.7",
"astro": "^4.9.0",
"nanostores": "^0.10.3",
"preact": "^10.21.0"
}
2 changes: 1 addition & 1 deletion examples/with-tailwindcss/package.json
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
"@astrojs/mdx": "^3.0.1",
"@astrojs/tailwind": "^5.1.0",
"@types/canvas-confetti": "^1.6.4",
"astro": "^4.8.7",
"astro": "^4.9.0",
"autoprefixer": "^10.4.19",
"canvas-confetti": "^1.9.3",
"postcss": "^8.4.38",
2 changes: 1 addition & 1 deletion examples/with-vitest/package.json
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
"test": "vitest"
},
"dependencies": {
"astro": "^4.8.7",
"astro": "^4.9.0",
"vitest": "^1.6.0"
}
}
181 changes: 181 additions & 0 deletions packages/astro/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,186 @@
# astro

## 4.9.0

### Minor Changes

- [#11051](https://github.com/withastro/astro/pull/11051) [`12a1bcc`](https://github.com/withastro/astro/commit/12a1bccc818af292cdd2a8ed0f3e3c042b9819b4) Thanks [@ematipico](https://github.com/ematipico)! - Introduces an experimental Container API to render `.astro` components in isolation.

This API introduces three new functions to allow you to create a new container and render an Astro component returning either a string or a Response:

- `create()`: creates a new instance of the container.
- `renderToString()`: renders a component and return a string.
- `renderToResponse()`: renders a component and returns the `Response` emitted by the rendering phase.

The first supported use of this new API is to enable unit testing. For example, with `vitest`, you can create a container to render your component with test data and check the result:

```js
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';

test('Card with slots', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Card, {
slots: {
default: 'Card content',
},
});

expect(result).toContain('This is a card');
expect(result).toContain('Card content');
});
```

For a complete reference, see the [Container API docs](/en/reference/container-reference/).

For a feature overview, and to give feedback on this experimental API, see the [Container API roadmap discussion](https://github.com/withastro/roadmap/pull/916).

- [#11021](https://github.com/withastro/astro/pull/11021) [`2d4c8fa`](https://github.com/withastro/astro/commit/2d4c8faa56a64d963fe7847b5be2d7a59e12ed5b) Thanks [@ematipico](https://github.com/ematipico)! - The CSRF protection feature that was introduced behind a flag in [v4.6.0](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md#460) is no longer experimental and is available for general use.

To enable the stable version, add the new top-level `security` option in `astro.config.mjs`. If you were previously using the experimental version of this feature, also delete the experimental flag:

```diff
export default defineConfig({
- experimental: {
- security: {
- csrfProtection: {
- origin: true
- }
- }
- },
+ security: {
+ checkOrigin: true
+ }
})
```

Enabling this setting performs a check that the `"origin"` header, automatically passed by all modern browsers, matches the URL sent by each Request.

This check is executed only for pages rendered on demand, and only for the requests `POST`, `PATCH`, `DELETE` and `PUT` with one of the following `"content-type"` headers: `'application/x-www-form-urlencoded'`, `'multipart/form-data'`, `'text/plain'`.

If the `"origin"` header doesn't match the pathname of the request, Astro will return a 403 status code and won't render the page.

For more information, see the [`security` configuration docs](https://docs.astro.build/en/reference/configuration-reference/#security).

- [#11022](https://github.com/withastro/astro/pull/11022) [`be68ab4`](https://github.com/withastro/astro/commit/be68ab47e236476ba980cbf74daf85f27cd866f4) Thanks [@ematipico](https://github.com/ematipico)! - The `i18nDomains` routing feature introduced behind a flag in [v3.4.0](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md#430) is no longer experimental and is available for general use.

This routing option allows you to configure different domains for individual locales in entirely server-rendered projects using the [@astrojs/node](https://docs.astro.build/en/guides/integrations-guide/node/) or [@astrojs/vercel](https://docs.astro.build/en/guides/integrations-guide/vercel/) adapter with a `site` configured.

If you were using this feature, please remove the experimental flag from your Astro config:

```diff
import { defineConfig } from 'astro'

export default defineConfig({
- experimental: {
- i18nDomains: true,
- }
})
```

If you have been waiting for stabilization before using this routing option, you can now do so.

Please see [the internationalization docs](https://docs.astro.build/en/guides/internationalization/#domains) for more about this feature.

- [#11071](https://github.com/withastro/astro/pull/11071) [`8ca7c73`](https://github.com/withastro/astro/commit/8ca7c731dea894e77f84b314ebe3a141d5daa918) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Adds two new functions `experimental_getActionState()` and `experimental_withState()` to support [the React 19 `useActionState()` hook](https://react.dev/reference/react/useActionState) when using Astro Actions. This introduces progressive enhancement when calling an Action with the `withState()` utility.

This example calls a `like` action that accepts a `postId` and returns the number of likes. Pass this action to the `experimental_withState()` function to apply progressive enhancement info, and apply to `useActionState()` to track the result:

```tsx
import { actions } from 'astro:actions';
import { experimental_withState } from '@astrojs/react/actions';

export function Like({ postId }: { postId: string }) {
const [state, action, pending] = useActionState(
experimental_withState(actions.like),
0 // initial likes
);

return (
<form action={action}>
<input type="hidden" name="postId" value={postId} />
<button disabled={pending}>{state} ❤️</button>
</form>
);
}
```

You can also access the state stored by `useActionState()` from your action `handler`. Call `experimental_getActionState()` with the API context, and optionally apply a type to the result:

```ts
import { defineAction, z } from 'astro:actions';
import { experimental_getActionState } from '@astrojs/react/actions';

export const server = {
like: defineAction({
input: z.object({
postId: z.string(),
}),
handler: async ({ postId }, ctx) => {
const currentLikes = experimental_getActionState<number>(ctx);
// write to database
return currentLikes + 1;
},
}),
};
```

- [#11101](https://github.com/withastro/astro/pull/11101) [`a6916e4`](https://github.com/withastro/astro/commit/a6916e4402bf5b7d74bab784a54eba63fd1d1179) Thanks [@linguofeng](https://github.com/linguofeng)! - Updates Astro's code for adapters to use the header `x-forwarded-for` to initialize the `clientAddress`.

To take advantage of the new change, integration authors must upgrade the version of Astro in their adapter `peerDependencies` to `4.9.0`.

- [#11071](https://github.com/withastro/astro/pull/11071) [`8ca7c73`](https://github.com/withastro/astro/commit/8ca7c731dea894e77f84b314ebe3a141d5daa918) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Adds compatibility for Astro Actions in the React 19 beta. Actions can be passed to a `form action` prop directly, and Astro will automatically add metadata for progressive enhancement.

```tsx
import { actions } from 'astro:actions';

function Like() {
return (
<form action={actions.like}>
{/* auto-inserts hidden input for progressive enhancement */}
<button type="submit">Like</button>
</form>
);
}
```

### Patch Changes

- [#11088](https://github.com/withastro/astro/pull/11088) [`9566fa0`](https://github.com/withastro/astro/commit/9566fa08608be766df355be17d72a39ea7b99ed0) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Allow actions to be called on the server. This allows you to call actions as utility functions in your Astro frontmatter, endpoints, and server-side UI components.

Import and call directly from `astro:actions` as you would for client actions:

```astro
---
// src/pages/blog/[postId].astro
import { actions } from 'astro:actions';

await actions.like({ postId: Astro.params.postId });
---
```

- [#11112](https://github.com/withastro/astro/pull/11112) [`29a8650`](https://github.com/withastro/astro/commit/29a8650375053cd5690a32bed4140f0fef11c705) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Deprecate the `getApiContext()` function. API Context can now be accessed from the second parameter to your Action `handler()`:

```diff
// src/actions/index.ts
import {
defineAction,
z,
- getApiContext,
} from 'astro:actions';

export const server = {
login: defineAction({
input: z.object({ id: z.string }),
+ handler(input, context) {
const user = context.locals.auth(input.id);
return user;
}
}),
}
```

## 4.8.7

### Patch Changes
16 changes: 16 additions & 0 deletions packages/astro/e2e/actions-blog.test.js
Original file line number Diff line number Diff line change
@@ -13,6 +13,11 @@ test.afterAll(async () => {
await devServer.stop();
});

test.afterEach(async ({ astro }) => {
// Force database reset between tests
await astro.editFile('./db/seed.ts', (original) => original);
});

test.describe('Astro Actions - Blog', () => {
test('Like action', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));
@@ -23,6 +28,17 @@ test.describe('Astro Actions - Blog', () => {
await expect(likeButton, 'like button should increment likes').toContainText('11');
});

test('Like action - server-side', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));

const likeButton = page.getByLabel('get-request');
const likeCount = page.getByLabel('Like');

await expect(likeCount, 'like button starts with 10 likes').toContainText('10');
await likeButton.click();
await expect(likeCount, 'like button should increment likes').toContainText('11');
});

test('Comment action - validation error', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));

61 changes: 61 additions & 0 deletions packages/astro/e2e/actions-react-19.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { expect } from '@playwright/test';
import { testFactory } from './test-utils.js';

const test = testFactory({ root: './fixtures/actions-react-19/' });

let devServer;

test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer();
});

test.afterEach(async ({ astro }) => {
// Force database reset between tests
await astro.editFile('./db/seed.ts', (original) => original);
});

test.afterAll(async () => {
await devServer.stop();
});

test.describe('Astro Actions - React 19', () => {
test('Like action - client pending state', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));

const likeButton = page.getByLabel('likes-client');
await expect(likeButton).toBeVisible();
await likeButton.click();
await expect(likeButton, 'like button should be disabled when pending').toBeDisabled();
await expect(likeButton).not.toBeDisabled({ timeout: 5000 });
});

test('Like action - server progressive enhancement', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));

const likeButton = page.getByLabel('likes-server');
await expect(likeButton, 'like button starts with 10 likes').toContainText('10');
await likeButton.click();

await expect(likeButton, 'like button increments').toContainText('11');
});

test('Like action - client useActionState', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));

const likeButton = page.getByLabel('likes-action-client');
await expect(likeButton).toBeVisible();
await likeButton.click();

await expect(likeButton, 'like button increments').toContainText('11');
});

test('Like action - server useActionState progressive enhancement', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/blog/first-post/'));

const likeButton = page.getByLabel('likes-action-server');
await expect(likeButton, 'like button starts with 10 likes').toContainText('10');
await likeButton.click();

await expect(likeButton, 'like button increments').toContainText('11');
});
});
2 changes: 1 addition & 1 deletion packages/astro/e2e/fixtures/actions-blog/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@e2e/astro-actions-basics",
"name": "@e2e/actions-blog",
"type": "module",
"version": "0.0.1",
"scripts": {
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const server = {
like: defineAction({
input: z.object({ postId: z.string() }),
handler: async ({ postId }) => {
await new Promise((r) => setTimeout(r, 200));
await new Promise((r) => setTimeout(r, 1000));

const { likes } = await db
.update(Likes)
Original file line number Diff line number Diff line change
@@ -17,11 +17,16 @@ export async function getStaticPaths() {
}));
}
type Props = CollectionEntry<'blog'>;
const post = await getEntry('blog', Astro.params.slug)!;
const { Content } = await post.render();
if (Astro.url.searchParams.has('like')) {
await actions.blog.like({postId: post.id });
}
const comment = Astro.getActionResult(actions.blog.comment);
const comments = await db.select().from(Comment).where(eq(Comment.postId, post.id));
@@ -35,6 +40,11 @@ const commentPostIdOverride = Astro.url.searchParams.get('commentPostIdOverride'
<BlogPost {...post.data}>
<Like postId={post.id} initial={initialLikes?.likes ?? 0} client:load />

<form>
<input type="hidden" name="like" />
<button type="submit" aria-label="get-request">Like GET request</button>
</form>

<Content />

<h2>Comments</h2>
17 changes: 17 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/astro.config.mjs
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,
},
});
21 changes: 21 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/db/config.ts
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 },
});
15 changes: 15 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/db/seed.ts
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!",
});
}
28 changes: 28 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@e2e/actions-react-19",
"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.6.0",
"@astrojs/db": "workspace:*",
"@astrojs/node": "workspace:*",
"@astrojs/react": "workspace:*",
"@types/react": "npm:types-react",
"@types/react-dom": "npm:types-react-dom",
"astro": "workspace:*",
"react": "19.0.0-beta-26f2496093-20240514",
"react-dom": "19.0.0-beta-26f2496093-20240514",
"typescript": "^5.4.5"
},
"overrides": {
"@types/react": "npm:types-react",
"@types/react-dom": "npm:types-react-dom"
}
}
47 changes: 47 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { db, Likes, eq, sql } from 'astro:db';
import { defineAction, getApiContext, z } from 'astro:actions';
import { experimental_getActionState } from '@astrojs/react/actions';

export const server = {
blog: {
like: defineAction({
accept: 'form',
input: z.object({ postId: z.string() }),
handler: async ({ postId }) => {
await new Promise((r) => setTimeout(r, 1000));

const { likes } = await db
.update(Likes)
.set({
likes: sql`likes + 1`,
})
.where(eq(Likes.postId, postId))
.returning()
.get();

return likes;
},
}),
likeWithActionState: defineAction({
accept: 'form',
input: z.object({ postId: z.string() }),
handler: async ({ postId }) => {
await new Promise((r) => setTimeout(r, 200));

const context = getApiContext();
const state = await experimental_getActionState<number>(context);

const { likes } = await db
.update(Likes)
.set({
likes: state + 1,
})
.where(eq(Likes.postId, postId))
.returning()
.get();

return likes;
},
}),
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
// 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} />

<!-- 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)} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
const today = new Date();
---

<footer>
&copy; {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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
interface Props {
date: Date;
}
const { date } = Astro.props;
---

<time datetime={date.toISOString()}>
{
date.toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
</time>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
import HeaderLink from './HeaderLink.astro';
import { SITE_TITLE } from '../consts';
---

<header>
<nav>
<h2><a href="/">{SITE_TITLE}</a></h2>
<div class="internal-links">
<HeaderLink href="/blog">Blog</HeaderLink>
</div>
<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"
><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"
><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"
><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>
</nav>
</header>
<style>
header {
margin: 0;
padding: 0 1em;
background: white;
box-shadow: 0 2px 8px rgba(var(--black), 5%);
}
h2 {
margin: 0;
font-size: 1em;
}

h2 a,
h2 a.active {
text-decoration: none;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
}
nav a {
padding: 1em 0.5em;
color: var(--black);
border-bottom: 4px solid transparent;
text-decoration: none;
}
nav a.active {
text-decoration: none;
border-bottom-color: var(--accent);
}
.social-links,
.social-links a {
display: flex;
}
@media (max-width: 720px) {
.social-links {
display: none;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
import type { HTMLAttributes } from 'astro/types';
type Props = HTMLAttributes<'a'>;
const { href, class: className, ...props } = Astro.props;
const { pathname } = Astro.url;
const subpath = pathname.match(/[^\/]+/g);
const isActive = href === pathname || href === '/' + subpath?.[0];
---

<a href={href} class:list={[className, { active: isActive }]} {...props}>
<slot />
</a>
<style>
a {
display: inline-block;
text-decoration: none;
}
a.active {
font-weight: bolder;
text-decoration: underline;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { actions } from 'astro:actions';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { experimental_withState } from '@astrojs/react/actions';

export function Like({ postId, label, likes }: { postId: string; label: string; likes: number }) {
return (
<form action={actions.blog.like}>
<input type="hidden" name="postId" value={postId} />
<Button likes={likes} label={label} />
</form>
);
}


export function LikeWithActionState({ postId, label, likes: initial }: { postId: string; label: string; likes: number }) {
const [likes, action] = useActionState(
experimental_withState(actions.blog.likeWithActionState),
10,
);

return (
<form action={action}>
<input type="hidden" name="postId" value={postId} />
<Button likes={likes} label={label} />
</form>
);
}

function Button({likes, label}: {likes: number; label: string}) {
const { pending } = useFormStatus();

return (
<button aria-label={label} disabled={pending} type="submit">
{likes} ❤️
</button>
)
}
5 changes: 5 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/src/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Place any global data in this file.
// You can import this data from anywhere in your site by using the `import` keyword.

export const SITE_TITLE = 'Astro Blog';
export const SITE_DESCRIPTION = 'Welcome to my website!';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: 'First post'
description: 'Lorem ipsum dolor sit amet'
pubDate: 'Jul 08 2022'
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.

Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.

Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.

Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.

Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
16 changes: 16 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/src/content/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
type: 'content',
// Type-check frontmatter using a schema
schema: z.object({
title: z.string(),
description: z.string(),
// Transform string to Date object
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
}),
});

export const collections = { blog };
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
import type { CollectionEntry } from 'astro:content';
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import FormattedDate from '../components/FormattedDate.astro';
type Props = CollectionEntry<'blog'>['data'];
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
---

<html lang="en">
<head>
<BaseHead title={title} description={description} />
<style>
main {
width: calc(100% - 2em);
max-width: 100%;
margin: 0;
}
.hero-image {
width: 100%;
}
.hero-image img {
display: block;
margin: 0 auto;
border-radius: 12px;
box-shadow: var(--box-shadow);
}
.prose {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 1em;
color: rgb(var(--gray-dark));
}
.title {
margin-bottom: 1em;
padding: 1em 0;
text-align: center;
line-height: 1;
}
.title h1 {
margin: 0 0 0.5em 0;
}
.date {
margin-bottom: 0.5em;
color: rgb(var(--gray));
}
.last-updated-on {
font-style: italic;
}
</style>
</head>

<body>
<Header />
<main>
<article>
<div class="hero-image">
{heroImage && <img width={1020} height={510} src={heroImage} alt="" />}
</div>
<div class="prose">
<div class="title">
<div class="date">
<FormattedDate date={pubDate} />
{
updatedDate && (
<div class="last-updated-on">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<h1>{title}</h1>
<hr />
</div>
<slot />
</div>
</article>
</main>
<Footer />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
import { type CollectionEntry, getCollection, getEntry } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
import { db, eq, Likes } from 'astro:db';
import { Like, LikeWithActionState } from '../../components/Like';
export const prerender = false;
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.slug },
props: post,
}));
}
type Props = CollectionEntry<'blog'>;
const post = await getEntry('blog', Astro.params.slug)!;
const { Content } = await post.render();
const likesRes = await db.select().from(Likes).where(eq(Likes.postId, post.id)).get()!;
---

<BlogPost {...post.data}>
<h2>Like</h2>
{
likesRes && (
<Like postId={post.id} likes={likesRes.likes} label="likes-client" client:load />
<Like postId={post.id} likes={likesRes.likes} label="likes-server" />
)
}

<h2>Like with action state</h2>
<LikeWithActionState postId={post.id} likes={10} label="likes-action-client" client:load />
<LikeWithActionState postId={post.id} likes={10} label="likes-action-server" />

<Content />

</BlogPost>
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
import BaseHead from '../../components/BaseHead.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
import { getCollection } from 'astro:content';
import FormattedDate from '../../components/FormattedDate.astro';
const posts = (await getCollection('blog')).sort(
(a, b) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf()
);
---

<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
<style>
main {
width: 960px;
}
ul {
display: flex;
flex-wrap: wrap;
gap: 2rem;
list-style-type: none;
margin: 0;
padding: 0;
}
ul li {
width: calc(50% - 1rem);
}
ul li * {
text-decoration: none;
transition: 0.2s ease;
}
ul li:first-child {
width: 100%;
margin-bottom: 1rem;
text-align: center;
}
ul li:first-child img {
width: 100%;
}
ul li:first-child .title {
font-size: 2.369rem;
}
ul li img {
margin-bottom: 0.5rem;
border-radius: 12px;
}
ul li a {
display: block;
}
.title {
margin: 0;
color: rgb(var(--black));
line-height: 1;
}
.date {
margin: 0;
color: rgb(var(--gray));
}
ul li a:hover h4,
ul li a:hover .date {
color: rgb(var(--accent));
}
ul a:hover img {
box-shadow: var(--box-shadow);
}
@media (max-width: 720px) {
ul {
gap: 0.5em;
}
ul li {
width: 100%;
text-align: center;
}
ul li:first-child {
margin-bottom: 0;
}
ul li:first-child .title {
font-size: 1.563em;
}
}
</style>
</head>
<body>
<Header />
<main>
<section>
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.slug}/`}>
<img width={720} height={360} src={post.data.heroImage} alt="" />
<h4 class="title">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
</a>
</li>
))
}
</ul>
</section>
</main>
<Footer />
</body>
</html>
140 changes: 140 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/src/styles/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
The CSS in this style tag is based off of Bear Blog's default CSS.
https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
*/

:root {
--accent: #2337ff;
--accent-dark: #000d8a;
--black: 15, 18, 25;
--gray: 96, 115, 159;
--gray-light: 229, 233, 240;
--gray-dark: 34, 41, 57;
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
--box-shadow: 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%),
0 16px 32px rgba(var(--gray), 33%);
}
body {
font-family: sans-serif;
margin: 0;
padding: 0;
text-align: left;
background: linear-gradient(var(--gray-gradient)) no-repeat;
background-size: 100% 600px;
word-wrap: break-word;
overflow-wrap: break-word;
color: rgb(var(--gray-dark));
font-size: 20px;
line-height: 1.7;
}
main {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 3em 1em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 0.5rem 0;
color: rgb(var(--black));
line-height: 1.2;
}
h1 {
font-size: 3.052em;
}
h2 {
font-size: 2.441em;
}
h3 {
font-size: 1.953em;
}
h4 {
font-size: 1.563em;
}
h5 {
font-size: 1.25em;
}
strong,
b {
font-weight: 700;
}
a {
color: var(--accent);
}
a:hover {
color: var(--accent);
}
p {
margin-bottom: 1em;
}
.prose p {
margin-bottom: 2em;
}
textarea {
width: 100%;
font-size: 16px;
}
input {
font-size: 16px;
}
table {
width: 100%;
}
img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
code {
padding: 2px 5px;
background-color: rgb(var(--gray-light));
border-radius: 2px;
}
pre {
padding: 1.5em;
border-radius: 8px;
}
pre > code {
all: unset;
}
blockquote {
border-left: 4px solid var(--accent);
padding: 0 0 0 20px;
margin: 0px;
font-size: 1.333em;
}
hr {
border: none;
border-top: 1px solid rgb(var(--gray-light));
}
@media (max-width: 720px) {
body {
font-size: 18px;
}
main {
padding: 1em;
}
}

.sr-only {
border: 0;
padding: 0;
margin: 0;
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px 1px 1px 1px);
/* maybe deprecated but we need to support legacy browsers */
clip: rect(1px, 1px, 1px, 1px);
/* modern browsers, clip-path works inwards from each corner */
clip-path: inset(50%);
/* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
white-space: nowrap;
}
8 changes: 8 additions & 0 deletions packages/astro/e2e/fixtures/actions-react-19/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"strictNullChecks": true,
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
6 changes: 5 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "astro",
"version": "4.8.7",
"version": "4.9.0",
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module",
"author": "withastro",
@@ -48,6 +48,10 @@
"types": "./config.d.ts",
"default": "./config.mjs"
},
"./container": {
"types": "./dist/container/index.d.ts",
"default": "./dist/container/index.js"
},
"./app": "./dist/core/app/index.js",
"./app/node": "./dist/core/app/node.js",
"./client/*": "./dist/runtime/client/*",
144 changes: 45 additions & 99 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
@@ -779,6 +779,49 @@ export interface AstroUserConfig {
*/
scopedStyleStrategy?: 'where' | 'class' | 'attribute';

/**
* @docs
* @name security
* @type {boolean}
* @default `{}`
* @version 4.9.0
* @description
*
* Enables security measures for an Astro website.
*
* These features only exist for pages rendered on demand (SSR) using `server` mode or pages that opt out of prerendering in `hybrid` mode.
*
* ```js
* // astro.config.mjs
* export default defineConfig({
* output: "server",
* security: {
* checkOrigin: true
* }
* })
* ```
*/
security?: {
/**
* @docs
* @name security.checkOrigin
* @kind h4
* @type {boolean}
* @default 'false'
* @version 4.9.0
* @description
*
* When enabled, performs a check that the "origin" header, automatically passed by all modern browsers, matches the URL sent by each `Request`. This is used to provide Cross-Site Request Forgery (CSRF) protection.
*
* The "origin" check is executed only for pages rendered on demand, and only for the requests `POST`, `PATCH`, `DELETE` and `PUT` with
* one of the following `content-type` headers: `'application/x-www-form-urlencoded'`, `'multipart/form-data'`, `'text/plain'`.
*
* If the "origin" header doesn't match the `pathname` of the request, Astro will return a 403 status code and will not render the page.
*/

checkOrigin?: boolean;
};

/**
* @docs
* @name vite
@@ -1956,105 +1999,6 @@ export interface AstroUserConfig {
*/
globalRoutePriority?: boolean;

/**
* @docs
* @name experimental.i18nDomains
* @type {boolean}
* @default `false`
* @version 4.3.0
* @description
*
* Enables domain support for the [experimental `domains` routing strategy](https://docs.astro.build/en/guides/internationalization/#domains-experimental) which allows you to configure the URL pattern of one or more supported languages to use a custom domain (or sub-domain).
*
* When a locale is mapped to a domain, a `/[locale]/` path prefix will not be used. However, localized folders within `src/pages/` are still required, including for your configured `defaultLocale`.
*
* Any other locale not configured will default to a localized path-based URL according to your `prefixDefaultLocale` strategy (e.g. `https://example.com/[locale]/blog`).
*
* ```js
* //astro.config.mjs
* export default defineConfig({
* site: "https://example.com",
* output: "server", // required, with no prerendered pages
* adapter: node({
* mode: 'standalone',
* }),
* i18n: {
* defaultLocale: "en",
* locales: ["en", "fr", "pt-br", "es"],
* prefixDefaultLocale: false,
* domains: {
* fr: "https://fr.example.com",
* es: "https://example.es",
* },
* },
* experimental: {
* i18nDomains: true,
* },
* });
* ```
*
* Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/reference/api-reference/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/reference/api-reference/#getabsolutelocaleurllist) will use the options set in `i18n.domains`.
*
* See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains-experimental) for more details, including the limitations of this experimental feature.
*/
i18nDomains?: boolean;

/**
* @docs
* @name experimental.security
* @type {boolean}
* @default `false`
* @version 4.6.0
* @description
*
* Enables CSRF protection for Astro websites.
*
* The CSRF protection works only for pages rendered on demand (SSR) using `server` or `hybrid` mode. The pages must opt out of prerendering in `hybrid` mode.
*
* ```js
* // astro.config.mjs
* export default defineConfig({
* output: "server",
* experimental: {
* security: {
* csrfProtection: {
* origin: true
* }
* }
* }
* })
* ```
*/
security?: {
/**
* @name security.csrfProtection
* @type {object}
* @default '{}'
* @version 4.6.0
* @description
*
* Allows you to enable security measures to prevent CSRF attacks: https://owasp.org/www-community/attacks/csrf
*/

csrfProtection?: {
/**
* @name security.csrfProtection.origin
* @type {boolean}
* @default 'false'
* @version 4.6.0
* @description
*
* When enabled, performs a check that the "origin" header, automatically passed by all modern browsers, matches the URL sent by each `Request`.
*
* The "origin" check is executed only for pages rendered on demand, and only for the requests `POST, `PATCH`, `DELETE` and `PUT` with
* the following `content-type` header: 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'.
*
* If the "origin" header doesn't match the `pathname` of the request, Astro will return a 403 status code and will not render the page.
*/
origin?: boolean;
};
};

/**
* @docs
* @name experimental.rewriting
@@ -3150,6 +3094,8 @@ export interface SSRResult {
): AstroGlobal;
resolve: (s: string) => Promise<string>;
response: AstroGlobal['response'];
request: AstroGlobal['request'];
actionResult?: ReturnType<AstroGlobal['getActionResult']>;
renderers: SSRLoadedRenderer[];
/**
* Map of directive name (e.g. `load`) to the directive script code
60 changes: 31 additions & 29 deletions packages/astro/src/actions/runtime/middleware.ts
Original file line number Diff line number Diff line change
@@ -8,44 +8,43 @@ import { callSafely } from './virtual/shared.js';
export type Locals = {
_actionsInternal: {
getActionResult: APIContext['getActionResult'];
actionResult?: ReturnType<APIContext['getActionResult']>;
};
};

export const onRequest = defineMiddleware(async (context, next) => {
const locals = context.locals as Locals;
// Actions middleware may have run already after a path rewrite.
// See https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md#ctxrewrite
// `_actionsInternal` is the same for every page,
// so short circuit if already defined.
if (locals._actionsInternal) return ApiContextStorage.run(context, () => next());
if (context.request.method === 'GET') {
return nextWithLocalsStub(next, locals);
return nextWithLocalsStub(next, context);
}

// Heuristic: If body is null, Astro might've reset this for prerendering.
// Stub with warning when `getActionResult()` is used.
if (context.request.method === 'POST' && context.request.body === null) {
return nextWithStaticStub(next, locals);
return nextWithStaticStub(next, context);
}

// Actions middleware may have run already after a path rewrite.
// See https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md#ctxrewrite
// `_actionsInternal` is the same for every page,
// so short circuit if already defined.
if (locals._actionsInternal) return next();

const { request, url } = context;
const contentType = request.headers.get('Content-Type');

// Avoid double-handling with middleware when calling actions directly.
if (url.pathname.startsWith('/_actions')) return nextWithLocalsStub(next, locals);
if (url.pathname.startsWith('/_actions')) return nextWithLocalsStub(next, context);

if (!contentType || !hasContentType(contentType, formContentTypes)) {
return nextWithLocalsStub(next, locals);
return nextWithLocalsStub(next, context);
}

const formData = await request.clone().formData();
const actionPath = formData.get('_astroAction');
if (typeof actionPath !== 'string') return nextWithLocalsStub(next, locals);
if (typeof actionPath !== 'string') return nextWithLocalsStub(next, context);

const actionPathKeys = actionPath.replace('/_actions/', '').split('.');
const action = await getAction(actionPathKeys);
if (!action) return nextWithLocalsStub(next, locals);
const action = await getAction(actionPath);
if (!action) return nextWithLocalsStub(next, context);

const result = await ApiContextStorage.run(context, () => callSafely(() => action(formData)));

@@ -56,21 +55,24 @@ export const onRequest = defineMiddleware(async (context, next) => {
// Cast to `any` to satisfy `getActionResult()` type.
return result as any;
},
actionResult: result,
};
Object.defineProperty(locals, '_actionsInternal', { writable: false, value: actionsInternal });
const response = await next();
if (result.error) {
return new Response(response.body, {
status: result.error.status,
statusText: result.error.name,
headers: response.headers,
});
}
return response;
return ApiContextStorage.run(context, async () => {
const response = await next();
if (result.error) {
return new Response(response.body, {
status: result.error.status,
statusText: result.error.name,
headers: response.headers,
});
}
return response;
});
});

function nextWithStaticStub(next: MiddlewareNext, locals: Locals) {
Object.defineProperty(locals, '_actionsInternal', {
function nextWithStaticStub(next: MiddlewareNext, context: APIContext) {
Object.defineProperty(context.locals, '_actionsInternal', {
writable: false,
value: {
getActionResult: () => {
@@ -82,15 +84,15 @@ function nextWithStaticStub(next: MiddlewareNext, locals: Locals) {
},
},
});
return next();
return ApiContextStorage.run(context, () => next());
}

function nextWithLocalsStub(next: MiddlewareNext, locals: Locals) {
Object.defineProperty(locals, '_actionsInternal', {
function nextWithLocalsStub(next: MiddlewareNext, context: APIContext) {
Object.defineProperty(context.locals, '_actionsInternal', {
writable: false,
value: {
getActionResult: () => undefined,
},
});
return next();
return ApiContextStorage.run(context, () => next());
}
3 changes: 1 addition & 2 deletions packages/astro/src/actions/runtime/route.ts
Original file line number Diff line number Diff line change
@@ -5,8 +5,7 @@ import { callSafely } from './virtual/shared.js';

export const POST: APIRoute = async (context) => {
const { request, url } = context;
const actionPathKeys = url.pathname.replace('/_actions/', '').split('.');
const action = await getAction(actionPathKeys);
const action = await getAction(url.pathname);
if (!action) {
return new Response(null, { status: 404 });
}
8 changes: 7 additions & 1 deletion packages/astro/src/actions/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -10,9 +10,15 @@ export function hasContentType(contentType: string, expected: string[]) {

export type MaybePromise<T> = T | Promise<T>;

/**
* Get server-side action based on the route path.
* Imports from `import.meta.env.ACTIONS_PATH`, which maps to
* the user's `src/actions/index.ts` file at build-time.
*/
export async function getAction(
pathKeys: string[]
path: string
): Promise<((param: unknown) => MaybePromise<unknown>) | undefined> {
const pathKeys = path.replace('/_actions/', '').split('.');
let { server: actionLookup } = await import(import.meta.env.ACTIONS_PATH);
for (const key of pathKeys) {
if (!(key in actionLookup)) {
23 changes: 11 additions & 12 deletions packages/astro/src/actions/runtime/virtual/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { getApiContext } from '../store.js';
import { type MaybePromise, hasContentType } from '../utils.js';
import { type ActionAPIContext, getApiContext as _getApiContext } from '../store.js';
import { type MaybePromise } from '../utils.js';
import {
ActionError,
ActionInputError,
@@ -13,16 +13,17 @@ export * from './shared.js';

export { z } from 'zod';

export { getApiContext } from '../store.js';
/** @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'
? z.AnyZodObject | z.ZodType<FormData>
: z.ZodType;

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

export type ActionClient<
TOutput,
@@ -88,13 +89,13 @@ function getFormServerHandler<TOutput, TInputSchema extends InputSchema<'form'>>
});
}

if (!(inputSchema instanceof z.ZodObject)) return await handler(unparsedInput);
if (!(inputSchema instanceof z.ZodObject)) return await handler(unparsedInput, getApiContext());

const parsed = await inputSchema.safeParseAsync(formDataToObject(unparsedInput, inputSchema));
if (!parsed.success) {
throw new ActionInputError(parsed.error.issues);
}
return await handler(parsed.data);
return await handler(parsed.data, getApiContext());
};
}

@@ -103,21 +104,19 @@ function getJsonServerHandler<TOutput, TInputSchema extends InputSchema<'json'>>
inputSchema?: TInputSchema
) {
return async (unparsedInput: unknown): Promise<Awaited<TOutput>> => {
const context = getApiContext();
const contentType = context.request.headers.get('content-type');
if (!contentType || !hasContentType(contentType, ['application/json'])) {
if (unparsedInput instanceof FormData) {
throw new ActionError({
code: 'UNSUPPORTED_MEDIA_TYPE',
message: 'This action only accepts JSON.',
});
}

if (!inputSchema) return await handler(unparsedInput);
if (!inputSchema) return await handler(unparsedInput, getApiContext());
const parsed = await inputSchema.safeParseAsync(unparsedInput);
if (!parsed.success) {
throw new ActionInputError(parsed.error.issues);
}
return await handler(parsed.data);
return await handler(parsed.data, getApiContext());
};
}

2 changes: 1 addition & 1 deletion packages/astro/src/actions/utils.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import type { APIContext } from '../@types/astro.js';
import { AstroError } from '../core/errors/errors.js';
import type { Locals } from './runtime/middleware.js';

function hasActionsInternal(locals: APIContext['locals']): locals is Locals {
export function hasActionsInternal(locals: APIContext['locals']): locals is Locals {
return '_actionsInternal' in locals;
}

424 changes: 424 additions & 0 deletions packages/astro/src/container/index.ts

Large diffs are not rendered by default.

114 changes: 114 additions & 0 deletions packages/astro/src/container/pipeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type {
ComponentInstance,
RewritePayload,
RouteData,
SSRElement,
SSRResult,
} from '../@types/astro.js';
import { type HeadElements, Pipeline } from '../core/base-pipeline.js';
import type { SinglePageBuiltModule } from '../core/build/types.js';
import { RouteNotFound } from '../core/errors/errors-data.js';
import { AstroError } from '../core/errors/index.js';
import {
createModuleScriptElement,
createStylesheetElementSet,
} from '../core/render/ssr-element.js';

export class ContainerPipeline extends Pipeline {
/**
* Internal cache to store components instances by `RouteData`.
* @private
*/
#componentsInterner: WeakMap<RouteData, SinglePageBuiltModule> = new WeakMap<
RouteData,
SinglePageBuiltModule
>();

static create({
logger,
manifest,
renderers,
resolve,
serverLike,
streaming,
}: Pick<
ContainerPipeline,
'logger' | 'manifest' | 'renderers' | 'resolve' | 'serverLike' | 'streaming'
>) {
return new ContainerPipeline(
logger,
manifest,
'development',
renderers,
resolve,
serverLike,
streaming
);
}

componentMetadata(_routeData: RouteData): Promise<SSRResult['componentMetadata']> | void {}

headElements(routeData: RouteData): Promise<HeadElements> | HeadElements {
const routeInfo = this.manifest.routes.find((route) => route.routeData === routeData);
const links = new Set<never>();
const scripts = new Set<SSRElement>();
const styles = createStylesheetElementSet(routeInfo?.styles ?? []);

for (const script of routeInfo?.scripts ?? []) {
if ('stage' in script) {
if (script.stage === 'head-inline') {
scripts.add({
props: {},
children: script.children,
});
}
} else {
scripts.add(createModuleScriptElement(script));
}
}
return { links, styles, scripts };
}

async tryRewrite(rewritePayload: RewritePayload): Promise<[RouteData, ComponentInstance]> {
let foundRoute: RouteData | undefined;
// options.manifest is the actual type that contains the information
for (const route of this.manifest.routes) {
const routeData = route.routeData;
if (rewritePayload instanceof URL) {
if (routeData.pattern.test(rewritePayload.pathname)) {
foundRoute = routeData;
break;
}
} else if (rewritePayload instanceof Request) {
const url = new URL(rewritePayload.url);
if (routeData.pattern.test(url.pathname)) {
foundRoute = routeData;
break;
}
} else if (routeData.pattern.test(decodeURI(rewritePayload))) {
foundRoute = routeData;
break;
}
}
if (foundRoute) {
const componentInstance = await this.getComponentByRoute(foundRoute);
return [foundRoute, componentInstance];
} else {
throw new AstroError(RouteNotFound);
}
}

insertRoute(route: RouteData, componentInstance: ComponentInstance): void {
this.#componentsInterner.set(route, {
page() {
return Promise.resolve(componentInstance);
},
renderers: this.manifest.renderers,
onRequest: this.manifest.middleware,
});
}

// At the moment it's not used by the container via any public API
// @ts-expect-error It needs to be implemented.
async getComponentByRoute(_routeData: RouteData): Promise<ComponentInstance> {}
}
6 changes: 5 additions & 1 deletion packages/astro/src/core/app/node.ts
Original file line number Diff line number Diff line change
@@ -81,7 +81,11 @@ export class NodeApp extends App {
Object.assign(options, makeRequestBody(req));
}
const request = new Request(url, options);
if (req.socket?.remoteAddress) {

const clientIp = req.headers['x-forwarded-for'];
if (clientIp) {
Reflect.set(request, clientAddressSymbol, clientIp);
} else if (req.socket?.remoteAddress) {
Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress);
}
return request;
8 changes: 4 additions & 4 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
@@ -129,8 +129,8 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
if (ssr) {
for (const [pageData, filePath] of pagesToGenerate) {
if (pageData.route.prerender) {
// i18n domains won't work with pre rendered routes at the moment, so we need to to throw an error
if (config.experimental.i18nDomains) {
// i18n domains won't work with pre rendered routes at the moment, so we need to throw an error
if (config.i18n?.domains && Object.keys(config.i18n.domains).length > 0) {
throw new AstroError({
...NoPrerenderedRoutesWithDomains,
message: NoPrerenderedRoutesWithDomains.message(pageData.component),
@@ -284,7 +284,7 @@ async function getPathsForRoute(
const label = staticPaths.length === 1 ? 'page' : 'pages';
logger.debug(
'build',
`├── ${bold(green(''))} ${route.component}${magenta(`[${staticPaths.length} ${label}]`)}`
`├── ${bold(green(''))} ${route.component}${magenta(`[${staticPaths.length} ${label}]`)}`
);

paths = staticPaths
@@ -558,6 +558,6 @@ function createBuildManifest(
buildFormat: settings.config.build.format,
middleware,
rewritingEnabled: settings.config.experimental.rewriting,
checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false,
checkOrigin: settings.config.security?.checkOrigin ?? false,
};
}
4 changes: 2 additions & 2 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
@@ -239,7 +239,7 @@ function buildManifest(
* logic meant for i18n domain support, where we fill the lookup table
*/
const i18n = settings.config.i18n;
if (settings.config.experimental.i18nDomains && i18n && i18n.domains) {
if (i18n && i18n.domains) {
for (const [locale, domainValue] of Object.entries(i18n.domains)) {
domainLookupTable[domainValue] = normalizeTheLocale(locale);
}
@@ -277,7 +277,7 @@ function buildManifest(
assets: staticFiles.map(prefixAssetPath),
i18n: i18nManifest,
buildFormat: settings.config.build.format,
checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false,
checkOrigin: settings.config.security?.checkOrigin ?? false,
rewritingEnabled: settings.config.experimental.rewriting,
};
}
53 changes: 22 additions & 31 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ type RehypePlugin = ComplexifyWithUnion<_RehypePlugin>;
type RemarkPlugin = ComplexifyWithUnion<_RemarkPlugin>;
type RemarkRehype = ComplexifyWithOmit<_RemarkRehype>;

const ASTRO_CONFIG_DEFAULTS = {
export const ASTRO_CONFIG_DEFAULTS = {
root: '.',
srcDir: './src',
publicDir: './public',
@@ -79,15 +79,14 @@ const ASTRO_CONFIG_DEFAULTS = {
vite: {},
legacy: {},
redirects: {},
security: {},
experimental: {
actions: false,
directRenderScript: false,
contentCollectionCache: false,
contentCollectionJsonSchema: false,
clientPrerender: false,
globalRoutePriority: false,
i18nDomains: false,
security: {},
rewriting: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };
@@ -493,6 +492,12 @@ export const AstroConfigSchema = z.object({
}
})
),
security: z
.object({
checkOrigin: z.boolean().default(false),
})
.optional()
.default(ASTRO_CONFIG_DEFAULTS.security),
experimental: z
.object({
actions: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.actions),
@@ -516,18 +521,6 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.globalRoutePriority),
security: z
.object({
csrfProtection: z
.object({
origin: z.boolean().default(false),
})
.optional()
.default({}),
})
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.security),
i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains),
rewriting: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rewriting),
})
.strict(
@@ -668,22 +661,20 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) {
})
.superRefine((configuration, ctx) => {
const { site, experimental, i18n, output } = configuration;
if (experimental.i18nDomains) {
const hasDomains = i18n?.domains ? Object.keys(i18n.domains).length > 0 : false;
if (hasDomains) {
if (!site) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"The option `site` isn't set. When using the 'domains' strategy for `i18n`, `site` is required to create absolute URLs for locales that aren't mapped to a domain.",
});
}
if (output !== 'server') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Domain support is only available when `output` is `"server"`.',
});
}
const hasDomains = i18n?.domains ? Object.keys(i18n.domains).length > 0 : false;
if (hasDomains) {
if (!site) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"The option `site` isn't set. When using the 'domains' strategy for `i18n`, `site` is required to create absolute URLs for locales that aren't mapped to a domain.",
});
}
if (output !== 'server') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Domain support is only available when `output` is `"server"`.',
});
}
}
});
15 changes: 12 additions & 3 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import type {
SSRResult,
} from '../@types/astro.js';
import type { ActionAPIContext } from '../actions/runtime/store.js';
import { createGetActionResult } from '../actions/utils.js';
import { createGetActionResult, hasActionsInternal } from '../actions/utils.js';
import {
computeCurrentLocale,
computePreferredLocale,
@@ -91,7 +91,10 @@ export class RenderContext {
* - endpoint
* - fallback
*/
async render(componentInstance: ComponentInstance | undefined): Promise<Response> {
async render(
componentInstance: ComponentInstance | undefined,
slots: Record<string, any> = {}
): Promise<Response> {
const { cookies, middleware, pathname, pipeline } = this;
const { logger, routeCache, serverLike, streaming } = pipeline;
const props = await getProps({
@@ -148,7 +151,7 @@ export class RenderContext {
result,
componentInstance?.default as any,
props,
{},
slots,
streaming,
this.routeData
);
@@ -294,6 +297,10 @@ export class RenderContext {
},
} satisfies AstroGlobal['response'];

const actionResult = hasActionsInternal(this.locals)
? this.locals._actionsInternal?.actionResult
: undefined;

// Create the result object that will be passed into the renderPage function.
// This object starts here as an empty shell (not yet the result) but then
// calling the render() function will populate the object with scripts, styles, etc.
@@ -313,8 +320,10 @@ export class RenderContext {
renderers,
resolve,
response,
request: this.request,
scripts,
styles,
actionResult,
_metadata: {
hasHydrationScript: false,
rendererSpecificHydrationScripts: new Set(),
19 changes: 9 additions & 10 deletions packages/astro/src/core/routing/manifest/create.ts
Original file line number Diff line number Diff line change
@@ -48,7 +48,7 @@ function countOccurrences(needle: string, haystack: string) {
const ROUTE_DYNAMIC_SPLIT = /\[(.+?\(.+?\)|.+?)\]/;
const ROUTE_SPREAD = /^\.{3}.+$/;

function getParts(part: string, file: string) {
export function getParts(part: string, file: string) {
const result: RoutePart[] = [];
part.split(ROUTE_DYNAMIC_SPLIT).map((str, i) => {
if (!str) return;
@@ -70,12 +70,11 @@ function getParts(part: string, file: string) {
return result;
}

function getPattern(
export function getPattern(
segments: RoutePart[][],
config: AstroConfig,
base: AstroConfig['base'],
addTrailingSlash: AstroConfig['trailingSlash']
) {
const base = config.base;
const pathname = segments
.map((segment) => {
if (segment.length === 1 && segment[0].spread) {
@@ -124,7 +123,7 @@ function getTrailingSlashPattern(addTrailingSlash: AstroConfig['trailingSlash'])
return '\\/?$';
}

function validateSegment(segment: string, file = '') {
export function validateSegment(segment: string, file = '') {
if (!file) file = segment;

if (/\]\[/.test(segment)) {
@@ -292,7 +291,7 @@ function createFileBasedRoutes(
components.push(item.file);
const component = item.file;
const { trailingSlash } = settings.config;
const pattern = getPattern(segments, settings.config, trailingSlash);
const pattern = getPattern(segments, settings.config.base, trailingSlash);
const generate = getRouteGenerator(segments, trailingSlash);
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
? `/${segments.map((segment) => segment[0].content).join('/')}`
@@ -363,7 +362,7 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Pri
const isPage = type === 'page';
const trailingSlash = isPage ? config.trailingSlash : 'never';

const pattern = getPattern(segments, settings.config, trailingSlash);
const pattern = getPattern(segments, settings.config.base, trailingSlash);
const generate = getRouteGenerator(segments, trailingSlash);
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
? `/${segments.map((segment) => segment[0].content).join('/')}`
@@ -419,7 +418,7 @@ function createRedirectRoutes(
return getParts(s, from);
});

const pattern = getPattern(segments, settings.config, trailingSlash);
const pattern = getPattern(segments, settings.config.base, trailingSlash);
const generate = getRouteGenerator(segments, trailingSlash);
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
? `/${segments.map((segment) => segment[0].content).join('/')}`
@@ -687,7 +686,7 @@ export function createRouteManifest(
pathname,
route,
segments,
pattern: getPattern(segments, config, config.trailingSlash),
pattern: getPattern(segments, config.base, config.trailingSlash),
type: 'fallback',
});
}
@@ -764,7 +763,7 @@ export function createRouteManifest(
route,
segments,
generate,
pattern: getPattern(segments, config, config.trailingSlash),
pattern: getPattern(segments, config.base, config.trailingSlash),
type: 'fallback',
fallbackRoutes: [],
};
2 changes: 1 addition & 1 deletion packages/astro/src/integrations/features-validation.ts
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@ export function validateSupportedFeatures(
);
validationResult.assets = validateAssetsFeature(assets, adapterName, config, logger);

if (i18nDomains && config?.experimental?.i18nDomains === true && !config.i18n?.domains) {
if (!config.i18n?.domains) {
validationResult.i18nDomains = validateSupportKind(
i18nDomains,
adapterName,
3 changes: 1 addition & 2 deletions packages/astro/src/vite-plugin-astro-server/plugin.ts
Original file line number Diff line number Diff line change
@@ -115,7 +115,6 @@ export default function createVitePluginAstroServer({
*
* Renderers needs to be pulled out from the page module emitted during the build.
* @param settings
* @param renderers
*/
export function createDevelopmentManifest(settings: AstroSettings): SSRManifest {
let i18nManifest: SSRManifestI18n | undefined = undefined;
@@ -144,7 +143,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
componentMetadata: new Map(),
inlinedScripts: new Map(),
i18n: i18nManifest,
checkOrigin: settings.config.experimental.security?.csrfProtection?.origin ?? false,
checkOrigin: settings.config.security?.checkOrigin ?? false,
rewritingEnabled: settings.config.experimental.rewriting,
middleware(_, next) {
return next();
49 changes: 37 additions & 12 deletions packages/astro/templates/actions.mjs
Original file line number Diff line number Diff line change
@@ -7,11 +7,33 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '/_actions/') {
return target[objKey];
}
const path = aggregatedPath + objKey.toString();
const action = (clientParam) => actionHandler(clientParam, path);
const action = (param) => actionHandler(param, path);
action.toString = () => path;
action.safe = (input) => {
return callSafely(() => action(input));
};
action.safe.toString = () => path;

// Add progressive enhancement info for React.
action.$$FORM_ACTION = function () {
const data = new FormData();
data.set('_astroAction', action.toString());
return {
method: 'POST',
name: action.toString(),
data,
};
};
action.safe.$$FORM_ACTION = function () {
const data = new FormData();
data.set('_astroAction', action.toString());
data.set('_astroActionSafe', 'true');
return {
method: 'POST',
name: action.toString(),
data,
};
};
// recurse to construct queries for nested object paths
// ex. actions.user.admins.auth()
return toActionProxy(action, path + '.');
@@ -20,24 +42,27 @@ function toActionProxy(actionCallback = {}, aggregatedPath = '/_actions/') {
}

/**
* @param {*} clientParam argument passed to the action when used on the client.
* @param {string} path Built path to call action on the server.
* Usage: `actions.[name](clientParam)`.
* @param {*} param argument passed to the action when called server or client-side.
* @param {string} path Built path to call action by path name.
* Usage: `actions.[name](param)`.
*/
async function actionHandler(clientParam, path) {
async function actionHandler(param, path) {
// When running server-side, import the action and call it.
if (import.meta.env.SSR) {
throw new ActionError({
code: 'BAD_REQUEST',
message:
'Action unexpectedly called on the server. If this error is unexpected, share your feedback on our RFC discussion: https://github.com/withastro/roadmap/pull/912',
});
const { getAction } = await import('astro/actions/runtime/utils.js');
const action = await getAction(path);
if (!action) throw new Error(`Action not found: ${path}`);

return action(param);
}

// When running client-side, make a fetch request to the action path.
const headers = new Headers();
headers.set('Accept', 'application/json');
let body = clientParam;
let body = param;
if (!(body instanceof FormData)) {
try {
body = clientParam ? JSON.stringify(clientParam) : undefined;
body = param ? JSON.stringify(param) : undefined;
} catch (e) {
throw new ActionError({
code: 'BAD_REQUEST',
11 changes: 11 additions & 0 deletions packages/astro/test/actions.test.js
Original file line number Diff line number Diff line change
@@ -214,5 +214,16 @@ describe('Astro Actions', () => {
const res = await app.render(req);
assert.equal(res.status, 204);
});

it('Is callable from the server with rewrite', async () => {
const req = new Request('http://example.com/rewrite');
const res = await app.render(req);
assert.equal(res.ok, true);

const html = await res.text();
let $ = cheerio.load(html);
assert.equal($('[data-url]').text(), '/subscribe');
assert.equal($('[data-channel]').text(), 'bholmesdev');
});
});
});
27 changes: 27 additions & 0 deletions packages/astro/test/client-address-node.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
import { createRequestAndResponse } from './units/test-utils.js';

describe('NodeClientAddress', () => {
it('clientAddress is 1.1.1.1', async () => {
const fixture = await loadFixture({
root: './fixtures/client-address-node/',
});
await fixture.build();
const handle = await fixture.loadNodeAdapterHandler();
const { req, res, text } = createRequestAndResponse({
method: 'GET',
url: '/',
headers: {
'x-forwarded-for': '1.1.1.1',
},
});
handle(req, res);
const html = await text();
const $ = cheerio.load(html);
assert.equal(res.statusCode, 200);
assert.equal($('#address').text(), '1.1.1.1');
});
});
142 changes: 142 additions & 0 deletions packages/astro/test/container.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { experimental_AstroContainer } from '../dist/container/index.js';
import {
Fragment,
createComponent,
maybeRenderHead,
render,
renderComponent,
renderHead,
renderSlot,
} from '../dist/runtime/server/index.js';

const BaseLayout = createComponent((result, _props, slots) => {
return render`<html>
<head>
${renderSlot(result, slots['head'])}
${renderHead(result)}
</head>
${maybeRenderHead(result)}
<body>
${renderSlot(result, slots['default'])}
</body>
</html>`;
});

describe('Container', () => {
it('Renders a div with hello world text', async () => {
const Page = createComponent((result) => {
return render`${renderComponent(
result,
'BaseLayout',
BaseLayout,
{},
{
default: () => render`${maybeRenderHead(result)}<div>hello world</div>`,
head: () => render`
${renderComponent(
result,
'Fragment',
Fragment,
{ slot: 'head' },
{
default: () => render`<meta charset="utf-8">`,
}
)}
`,
}
)}`;
});

const container = await experimental_AstroContainer.create();
const response = await container.renderToString(Page);

assert.match(response, /hello world/);
});

it('Renders a slot', async () => {
const Page = createComponent(
(result, _props, slots) => {
return render`${renderComponent(
result,
'BaseLayout',
BaseLayout,
{},
{
default: () => render`
${maybeRenderHead(result)}
${renderSlot(result, slots['default'])}
`,
head: () => render`
${renderComponent(
result,
'Fragment',
Fragment,
{ slot: 'head' },
{
default: () => render`<meta charset="utf-8">`,
}
)}
`,
}
)}`;
},
'Component2.astro',
undefined
);

const container = await experimental_AstroContainer.create();
const result = await container.renderToString(Page, {
slots: {
default: 'some slot',
},
});

assert.match(result, /some slot/);
});

it('Renders multiple named slots', async () => {
const Page = createComponent(
(result, _props, slots) => {
return render`${renderComponent(
result,
'BaseLayout',
BaseLayout,
{},
{
default: () => render`
${maybeRenderHead(result)}
${renderSlot(result, slots['custom-name'])}
${renderSlot(result, slots['foo-name'])}
`,
head: () => render`
${renderComponent(
result,
'Fragment',
Fragment,
{ slot: 'head' },
{
default: () => render`<meta charset="utf-8">`,
}
)}
`,
}
)}`;
},
'Component2.astro',
undefined
);

const container = await experimental_AstroContainer.create();
const result = await container.renderToString(Page, {
slots: {
'custom-name': 'Custom name',
'foo-name': 'Bar name',
},
});

assert.match(result, /Custom name/);
assert.match(result, /Bar name/);
});
});
19 changes: 14 additions & 5 deletions packages/astro/test/fixtures/actions/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineAction, getApiContext, ActionError, z } from 'astro:actions';
import { defineAction, ActionError, z } from 'astro:actions';

export const server = {
subscribe: defineAction({
@@ -10,6 +10,17 @@ export const server = {
};
},
}),
subscribeFromServer: defineAction({
input: z.object({ channel: z.string() }),
handler: async ({ channel }, { url }) => {
return {
// Returned to ensure path rewrites are respected
url: url.pathname,
channel,
subscribeButtonState: 'smashed',
};
},
}),
comment: defineAction({
accept: 'form',
input: z.object({ channel: z.string(), comment: z.string() }),
@@ -31,15 +42,13 @@ export const server = {
}),
getUser: defineAction({
accept: 'form',
handler: async () => {
const { locals } = getApiContext();
handler: async (_, { locals }) => {
return locals.user;
}
}),
getUserOrThrow: defineAction({
accept: 'form',
handler: async () => {
const { locals } = getApiContext();
handler: async (_, { locals }) => {
if (locals.user?.name !== 'admin') {
// Expected to throw
throw new ActionError({
3 changes: 3 additions & 0 deletions packages/astro/test/fixtures/actions/src/pages/rewrite.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
return Astro.rewrite('/subscribe');
---
11 changes: 11 additions & 0 deletions packages/astro/test/fixtures/actions/src/pages/subscribe.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
import { actions } from 'astro:actions';

const { url, channel } = await actions.subscribeFromServer({
channel: 'bholmesdev',
});
---

<p data-url>{url}</p>
<p data-channel>{channel}</p>

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import node from '@astrojs/node';
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
output: 'server',
adapter: node({ mode: 'middleware' }),
});
9 changes: 9 additions & 0 deletions packages/astro/test/fixtures/client-address-node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/client-address-node",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/node": "workspace:*",
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
export const prerender = false;
const address = Astro.clientAddress;
---
<html>
<head>
<title>Astro.clientAddress</title>
</head>
<body>
<h1>Astro.clientAddress</h1>
<div id="address">{ address }</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -3,12 +3,8 @@ import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
output: "server",
experimental: {
security: {
csrfProtection: {
origin: true
}
}
security: {
checkOrigin: true
}
});

Original file line number Diff line number Diff line change
@@ -17,8 +17,5 @@ export default defineConfig({
redirectToDefaultLocale: false
}
},
experimental: {
i18nDomains: true
},
site: "https://example.com",
})
8 changes: 8 additions & 0 deletions packages/astro/test/test-utils.js
Original file line number Diff line number Diff line change
@@ -25,6 +25,8 @@ process.env.ASTRO_TELEMETRY_DISABLED = true;
* @typedef {import('../src/core/app/index').App} App
* @typedef {import('../src/cli/check/index').AstroChecker} AstroChecker
* @typedef {import('../src/cli/check/index').CheckPayload} CheckPayload
* @typedef {import('http').IncomingMessage} NodeRequest
* @typedef {import('http').ServerResponse} NodeResponse
*
*
* @typedef {Object} Fixture
@@ -40,6 +42,7 @@ process.env.ASTRO_TELEMETRY_DISABLED = true;
* @property {typeof preview} preview
* @property {() => Promise<void>} clean
* @property {() => Promise<App>} loadTestAdapterApp
* @property {() => Promise<(req: NodeRequest, res: NodeResponse) => void>} loadNodeAdapterHandler
* @property {() => Promise<void>} onNextChange
* @property {typeof check} check
* @property {typeof sync} sync
@@ -213,6 +216,11 @@ export async function loadFixture(inlineConfig) {
});
}
},
loadNodeAdapterHandler: async () => {
const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir);
const { handler } = await import(url);
return handler;
},
loadTestAdapterApp: async (streaming) => {
const url = new URL(`./server/entry.mjs?id=${fixtureId}`, config.outDir);
const { createApp, manifest } = await import(url);
6 changes: 0 additions & 6 deletions packages/astro/test/units/config/config-validate.test.js
Original file line number Diff line number Diff line change
@@ -319,9 +319,6 @@ describe('Config Validation', () => {
en: 'https://www.example.com/',
},
},
experimental: {
i18nDomains: true,
},
},
process.cwd()
).catch((err) => err);
@@ -343,9 +340,6 @@ describe('Config Validation', () => {
en: 'https://www.example.com/',
},
},
experimental: {
i18nDomains: true,
},
site: 'https://foo.org',
},
process.cwd()
27 changes: 13 additions & 14 deletions packages/astro/test/units/i18n/astro_i18n.test.js
Original file line number Diff line number Diff line change
@@ -1548,6 +1548,7 @@ describe('getLocaleAbsoluteUrlList', () => {
const config = await validateConfig(
{
format: 'directory',
output: 'server',
site: 'https://example.com/',
trailingSlash: 'always',
i18n: {
@@ -1587,27 +1588,25 @@ describe('getLocaleAbsoluteUrlList', () => {
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
experimental: {
i18n: {
defaultLocale: 'en',
locales: [
'en',
'en_US',
'es',
{
path: 'italiano',
codes: ['it', 'it-VA'],
},
],
},
i18n: {
defaultLocale: 'en',
locales: [
'en',
'en_US',
'es',
{
path: 'italiano',
codes: ['it', 'it-VA'],
},
],
},
};
// directory format
assert.deepEqual(
getLocaleAbsoluteUrlList({
locale: 'en',
base: '/blog/',
...config.experimental.i18n,
...config.i18n,
trailingSlash: 'always',
format: 'file',
site: 'https://example.com',
6 changes: 1 addition & 5 deletions packages/db/package.json
Original file line number Diff line number Diff line change
@@ -66,8 +66,7 @@
"build": "astro-scripts build \"src/**/*.ts\" && tsc && pnpm types:virtual",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --exit --timeout 20000 \"test/*.js\" \"test/unit/**/*.js\"",
"test:match": "mocha --timeout 20000 \"test/*.js\" \"test/unit/*.js\" -g"
"test": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
"@astrojs/studio": "workspace:*",
@@ -90,14 +89,11 @@
"@types/chai": "^4.3.16",
"@types/deep-diff": "^1.0.5",
"@types/diff": "^5.2.1",
"@types/mocha": "^10.0.6",
"@types/prompts": "^2.4.9",
"@types/yargs-parser": "^21.0.3",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^5.1.1",
"cheerio": "1.0.0-rc.12",
"mocha": "^10.4.0",
"typescript": "^5.4.5",
"vite": "^5.2.11"
}
41 changes: 21 additions & 20 deletions packages/db/test/basics.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { load as cheerioLoad } from 'cheerio';
import testAdapter from '../../astro/test/test-adapter.js';
import { loadFixture } from '../../astro/test/test-utils.js';
@@ -30,53 +31,53 @@ describe('astro:db', () => {
const $ = cheerioLoad(html);

const ul = $('.authors-list');
expect(ul.children()).to.have.a.lengthOf(5);
expect(ul.children().eq(0).text()).to.equal('Ben');
assert.equal(ul.children().length, 5);
assert.match(ul.children().eq(0).text(), /Ben/);
});

it('Allows expression defaults for date columns', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const themeAdded = $($('.themes-list .theme-added')[0]).text();
expect(new Date(themeAdded).getTime()).to.not.be.NaN;
assert.equal(Number.isNaN(new Date(themeAdded).getTime()), false);
});

it('Defaults can be overridden for dates', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const themeAdded = $($('.themes-list .theme-added')[1]).text();
expect(new Date(themeAdded).getTime()).to.not.be.NaN;
assert.equal(Number.isNaN(new Date(themeAdded).getTime()), false);
});

it('Allows expression defaults for text columns', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const themeOwner = $($('.themes-list .theme-owner')[0]).text();
expect(themeOwner).to.equal('');
assert.equal(themeOwner, '');
});

it('Allows expression defaults for boolean columns', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const themeDark = $($('.themes-list .theme-dark')[0]).text();
expect(themeDark).to.equal('dark mode');
assert.match(themeDark, /dark mode/);
});

it('text fields an be used as references', async () => {
const html = await fixture.fetch('/login').then((res) => res.text());
const $ = cheerioLoad(html);

expect($('.session-id').text()).to.equal('12345');
expect($('.username').text()).to.equal('Mario');
assert.match($('.session-id').text(), /12345/);
assert.match($('.username').text(), /Mario/);
});

it('Prints authors from raw sql call', async () => {
const json = await fixture.fetch('run.json').then((res) => res.json());
expect(json).to.deep.equal({
assert.deepEqual(json, {
columns: ['_id', 'name', 'age2'],
columnTypes: ['INTEGER', 'TEXT', 'INTEGER'],
rows: [
@@ -111,53 +112,53 @@ describe('astro:db', () => {
const $ = cheerioLoad(html);

const ul = $('.authors-list');
expect(ul.children()).to.have.a.lengthOf(5);
expect(ul.children().eq(0).text()).to.equal('Ben');
assert.equal(ul.children().length, 5);
assert.match(ul.children().eq(0).text(), /Ben/);
});

it('Allows expression defaults for date columns', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const themeAdded = $($('.themes-list .theme-added')[0]).text();
expect(new Date(themeAdded).getTime()).to.not.be.NaN;
assert.equal(Number.isNaN(new Date(themeAdded).getTime()), false);
});

it('Defaults can be overridden for dates', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const themeAdded = $($('.themes-list .theme-added')[1]).text();
expect(new Date(themeAdded).getTime()).to.not.be.NaN;
assert.equal(Number.isNaN(new Date(themeAdded).getTime()), false);
});

it('Allows expression defaults for text columns', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const themeOwner = $($('.themes-list .theme-owner')[0]).text();
expect(themeOwner).to.equal('');
assert.equal(themeOwner, '');
});

it('Allows expression defaults for boolean columns', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const themeDark = $($('.themes-list .theme-dark')[0]).text();
expect(themeDark).to.equal('dark mode');
assert.match(themeDark, /dark mode/);
});

it('text fields an be used as references', async () => {
const html = await fixture.fetch('/login').then((res) => res.text());
const $ = cheerioLoad(html);

expect($('.session-id').text()).to.equal('12345');
expect($('.username').text()).to.equal('Mario');
assert.match($('.session-id').text(), /12345/);
assert.match($('.username').text(), /Mario/);
});

it('Prints authors from raw sql call', async () => {
const json = await fixture.fetch('run.json').then((res) => res.json());
expect(json).to.deep.equal({
assert.deepEqual(json, {
columns: ['_id', 'name', 'age2'],
columnTypes: ['INTEGER', 'TEXT', 'INTEGER'],
rows: [
@@ -195,7 +196,7 @@ describe('astro:db', () => {
const $ = cheerioLoad(html);

const ul = $('.authors-list');
expect(ul.children()).to.have.a.lengthOf(5);
assert.equal(ul.children().length, 5);
});
});
});
7 changes: 4 additions & 3 deletions packages/db/test/db-in-src.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { load as cheerioLoad } from 'cheerio';
import testAdapter from '../../astro/test/test-adapter.js';
import { loadFixture } from '../../astro/test/test-utils.js';
@@ -30,8 +31,8 @@ describe('astro:db', () => {
const $ = cheerioLoad(html);

const ul = $('.users-list');
expect(ul.children()).to.have.a.lengthOf(1);
expect($('.users-list li').text()).to.equal('Mario');
assert.equal(ul.children().length, 1);
assert.match($('.users-list li').text(), /Mario/);
});
});
});
7 changes: 4 additions & 3 deletions packages/db/test/error-handling.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { loadFixture } from '../../astro/test/test-utils.js';
import { setupRemoteDbServer } from './test-utils.js';

@@ -26,7 +27,7 @@ describe('astro:db - error handling', () => {

it('Raises foreign key constraint LibsqlError', async () => {
const json = await fixture.fetch('/foreign-key-constraint.json').then((res) => res.json());
expect(json).to.deep.equal({
assert.deepEqual(json, {
message: foreignKeyConstraintError,
code: 'SQLITE_CONSTRAINT_FOREIGNKEY',
});
@@ -47,7 +48,7 @@ describe('astro:db - error handling', () => {

it('Raises foreign key constraint LibsqlError', async () => {
const json = await fixture.readFile('/foreign-key-constraint.json');
expect(JSON.parse(json)).to.deep.equal({
assert.deepEqual(JSON.parse(json), {
message: foreignKeyConstraintError,
code: 'SQLITE_CONSTRAINT_FOREIGNKEY',
});
11 changes: 6 additions & 5 deletions packages/db/test/integration-only.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from '../../astro/test/test-utils.js';

@@ -25,8 +26,8 @@ describe('astro:db with only integrations, no user db config', () => {
const $ = cheerioLoad(html);

const ul = $('ul.menu');
expect(ul.children()).to.have.a.lengthOf(4);
expect(ul.children().eq(0).text()).to.equal('Pancakes');
assert.equal(ul.children().length, 4);
assert.match(ul.children().eq(0).text(), /Pancakes/);
});
});

@@ -40,8 +41,8 @@ describe('astro:db with only integrations, no user db config', () => {
const $ = cheerioLoad(html);

const ul = $('ul.menu');
expect(ul.children()).to.have.a.lengthOf(4);
expect(ul.children().eq(0).text()).to.equal('Pancakes');
assert.equal(ul.children().length, 4);
assert.match(ul.children().eq(0).text(), /Pancakes/);
});
});
});
19 changes: 10 additions & 9 deletions packages/db/test/integrations.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from '../../astro/test/test-utils.js';

@@ -27,17 +28,17 @@ describe('astro:db with integrations', () => {
const $ = cheerioLoad(html);

const ul = $('.authors-list');
expect(ul.children()).to.have.a.lengthOf(5);
expect(ul.children().eq(0).text()).to.equal('Ben');
assert.equal(ul.children().length, 5);
assert.match(ul.children().eq(0).text(), /Ben/);
});

it('Prints the list of menu items from integration-defined table', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);

const ul = $('ul.menu');
expect(ul.children()).to.have.a.lengthOf(4);
expect(ul.children().eq(0).text()).to.equal('Pancakes');
assert.equal(ul.children().length, 4);
assert.match(ul.children().eq(0).text(), /Pancakes/);
});
});

@@ -51,17 +52,17 @@ describe('astro:db with integrations', () => {
const $ = cheerioLoad(html);

const ul = $('.authors-list');
expect(ul.children()).to.have.a.lengthOf(5);
expect(ul.children().eq(0).text()).to.equal('Ben');
assert.equal(ul.children().length, 5);
assert.match(ul.children().eq(0).text(), /Ben/);
});

it('Prints the list of menu items from integration-defined table', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerioLoad(html);

const ul = $('ul.menu');
expect(ul.children()).to.have.a.lengthOf(4);
expect(ul.children().eq(0).text()).to.equal('Pancakes');
assert.equal(ul.children().length, 4);
assert.match(ul.children().eq(0).text(), /Pancakes/);
});
});
});
11 changes: 6 additions & 5 deletions packages/db/test/local-prod.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { relative } from 'path';
import { fileURLToPath } from 'url';
import { expect } from 'chai';
import testAdapter from '../../astro/test/test-adapter.js';
import { loadFixture } from '../../astro/test/test-utils.js';

@@ -29,7 +30,7 @@ describe('astro:db local database', () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
expect(response.status).to.equal(200);
assert.equal(response.status, 200);
});
});

@@ -50,7 +51,7 @@ describe('astro:db local database', () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
expect(response.status).to.equal(200);
assert.equal(response.status, 200);
});
});

@@ -64,7 +65,7 @@ describe('astro:db local database', () => {
buildError = err;
}

expect(buildError).to.be.an('Error');
assert.equal(buildError instanceof Error, true);
});

it('should throw during the build for hybrid output', async () => {
@@ -82,7 +83,7 @@ describe('astro:db local database', () => {
buildError = err;
}

expect(buildError).to.be.an('Error');
assert.equal(buildError instanceof Error, true);
});
});
});
11 changes: 6 additions & 5 deletions packages/db/test/no-seed.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from '../../astro/test/test-utils.js';

@@ -25,8 +26,8 @@ describe('astro:db with no seed file', () => {
const $ = cheerioLoad(html);

const ul = $('.authors-list');
expect(ul.children()).to.have.a.lengthOf(5);
expect(ul.children().eq(0).text()).to.equal('Ben');
assert.equal(ul.children().length, 5);
assert.match(ul.children().eq(0).text(), /Ben/);
});
});

@@ -40,8 +41,8 @@ describe('astro:db with no seed file', () => {
const $ = cheerioLoad(html);

const ul = $('.authors-list');
expect(ul.children()).to.have.a.lengthOf(5);
expect(ul.children().eq(0).text()).to.equal('Ben');
assert.equal(ul.children().length, 5);
assert.match(ul.children().eq(0).text(), /Ben/);
});
});
});
7 changes: 4 additions & 3 deletions packages/db/test/ssr-no-apptoken.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import testAdapter from '../../astro/test/test-adapter.js';
import { loadFixture } from '../../astro/test/test-utils.js';
import { setupRemoteDbServer } from './test-utils.js';
@@ -26,11 +27,11 @@ describe('missing app token', () => {
it('Errors as runtime', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
try {
const response = await app.render(request);
await response.text();
} catch {
expect(response.status).to.equal(501);
assert.equal(response.status, 501);
}
});
});
7 changes: 4 additions & 3 deletions packages/db/test/static-remote.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture } from '../../astro/test/test-utils.js';
import { setupRemoteDbServer } from './test-utils.js';
@@ -28,14 +29,14 @@ describe('astro:db', () => {
const html = await fixture.readFile('/index.html');
const $ = cheerioLoad(html);

expect($('li').length).to.equal(1);
assert.equal($('li').length, 1);
});

it('Returns correct shape from db.run()', async () => {
const html = await fixture.readFile('/run/index.html');
const $ = cheerioLoad(html);

expect($('#row').text()).to.equal('1');
assert.match($('#row').text(), /1/);
});
});
});
Loading