diff options
Diffstat (limited to 'packages/astro')
164 files changed, 1104 insertions, 579 deletions
diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index cbd3aedc0..3b6c119e8 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -53,7 +53,6 @@ </script> ``` - - [#11788](https://github.com/withastro/astro/pull/11788) [`7c0ccfc`](https://github.com/withastro/astro/commit/7c0ccfc26947b178584e3476584bcaa490c6ba86) Thanks [@ematipico](https://github.com/ematipico)! - Updates the default value of `security.checkOrigin` to `true`, which enables Cross-Site Request Forgery (CSRF) protection by default for pages rendered on demand. If you had previously configured `security.checkOrigin: true`, you no longer need this set in your Astro config. This is now the default and it is safe to remove. @@ -109,6 +108,42 @@ If you are using this service, and cannot migrate to the base Sharp image service, a third-party extraction of the previous service is available here: https://github.com/Princesseuh/astro-image-service-squoosh +## 4.14.6 + +### Patch Changes + +- [#11847](https://github.com/withastro/astro/pull/11847) [`45b599c`](https://github.com/withastro/astro/commit/45b599c4d40ded6a3e03881181b441ae494cbfcf) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a case where Vite would be imported by the SSR runtime, causing bundling errors and bloat. + +- [#11822](https://github.com/withastro/astro/pull/11822) [`6fcaab8`](https://github.com/withastro/astro/commit/6fcaab84de1044ff4d186b2dfa5831964460062d) Thanks [@bluwy](https://github.com/bluwy)! - Marks internal `vite-plugin-fileurl` plugin with `enforce: 'pre'` + +- [#11713](https://github.com/withastro/astro/pull/11713) [`497324c`](https://github.com/withastro/astro/commit/497324c4e87538dc1dc13aea3ced9bd3642d9ba6) Thanks [@voidfill](https://github.com/voidfill)! - Prevents prefetching of the same urls with different hashes. + +- [#11814](https://github.com/withastro/astro/pull/11814) [`2bb72c6`](https://github.com/withastro/astro/commit/2bb72c63969f8f21dd279fa927c32f192ff79a3f) Thanks [@eduardocereto](https://github.com/eduardocereto)! - Updates the documentation for experimental Content Layer API with a corrected code example + +- [#11842](https://github.com/withastro/astro/pull/11842) [`1ffaae0`](https://github.com/withastro/astro/commit/1ffaae04cf790390f730bf900b9722b99642adc1) Thanks [@stephan281094](https://github.com/stephan281094)! - Fixes a typo in the `MissingImageDimension` error message + +- [#11828](https://github.com/withastro/astro/pull/11828) [`20d47aa`](https://github.com/withastro/astro/commit/20d47aa85a3a0d7ac3390f749715d92de830cf3e) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Improves error message when invalid data is returned by an Action. + +## 4.14.5 + +### Patch Changes + +- [#11809](https://github.com/withastro/astro/pull/11809) [`62e97a2`](https://github.com/withastro/astro/commit/62e97a20f72bacb017c633ddcb776abc89167660) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fixes usage of `.transform()`, `.refine()`, `.passthrough()`, and other effects on Action form inputs. + +- [#11812](https://github.com/withastro/astro/pull/11812) [`260c4be`](https://github.com/withastro/astro/commit/260c4be050f91353bc5ba6af073e7bc17429d552) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Exposes `ActionAPIContext` type from the `astro:actions` module. + +- [#11813](https://github.com/withastro/astro/pull/11813) [`3f7630a`](https://github.com/withastro/astro/commit/3f7630afd697809b1d4fbac6edd18153983c70ac) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fixes unexpected `undefined` value when calling an action from the client without a return value. + +## 4.14.4 + +### Patch Changes + +- [#11794](https://github.com/withastro/astro/pull/11794) [`3691a62`](https://github.com/withastro/astro/commit/3691a626fb67d617e5f8bd057443cd2ff6caa054) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fixes unexpected warning log when using Actions on "hybrid" rendered projects. + +- [#11801](https://github.com/withastro/astro/pull/11801) [`9f943c1`](https://github.com/withastro/astro/commit/9f943c1344671b569a0d1ddba683b3cca0068adc) Thanks [@delucis](https://github.com/delucis)! - Fixes a bug where the `filePath` property was not available on content collection entries when using the content layer `file()` loader with a JSON file that contained an object instead of an array. This was breaking use of the `image()` schema utility among other things. + +## 4.14.3 + ### Patch Changes - [#11780](https://github.com/withastro/astro/pull/11780) [`c6622ad`](https://github.com/withastro/astro/commit/c6622adaeb405e961b12c91f0e5d02c7333d01cf) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Deprecates the Squoosh image service, to be removed in Astro 5.0. We recommend migrating to the default Sharp service. @@ -127,6 +162,16 @@ - [#11774](https://github.com/withastro/astro/pull/11774) [`c6400ab`](https://github.com/withastro/astro/commit/c6400ab99c5e5f4477bc6ef7e801b7869b0aa9ab) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes the path returned by `injectTypes` +- [#11790](https://github.com/withastro/astro/pull/11790) [`41c3fcb`](https://github.com/withastro/astro/commit/41c3fcb6189709450a67ea8f726071d5f3cdc80e) Thanks [@sarah11918](https://github.com/sarah11918)! - Updates the documentation for experimental `astro:env` with a corrected link to the RFC proposal + +- [#11773](https://github.com/withastro/astro/pull/11773) [`86a3391`](https://github.com/withastro/astro/commit/86a33915ff41b23ff6b35bcfb1805fefc0760ca7) Thanks [@ematipico](https://github.com/ematipico)! - Changes messages logged when using unsupported, deprecated, or experimental adapter features for clarity + +- [#11745](https://github.com/withastro/astro/pull/11745) [`89bab1e`](https://github.com/withastro/astro/commit/89bab1e70786123fbe933a9d7a1b80c9334dcc5f) Thanks [@bluwy](https://github.com/bluwy)! - Prints prerender dynamic value usage warning only if it's used + +- [#11774](https://github.com/withastro/astro/pull/11774) [`c6400ab`](https://github.com/withastro/astro/commit/c6400ab99c5e5f4477bc6ef7e801b7869b0aa9ab) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes the path returned by `injectTypes` + +- [#11730](https://github.com/withastro/astro/pull/11730) [`2df49a6`](https://github.com/withastro/astro/commit/2df49a6fb4f6d92fe45f7429430abe63defeacd6) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Simplifies path operations of `astro sync` + - [#11771](https://github.com/withastro/astro/pull/11771) [`49650a4`](https://github.com/withastro/astro/commit/49650a45550af46c70c6cf3f848b7b529103a649) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes an error thrown by `astro sync` when an `astro:env` virtual module is imported inside the Content Collections config - [#11744](https://github.com/withastro/astro/pull/11744) [`b677429`](https://github.com/withastro/astro/commit/b67742961a384c10e5cd04cf5b02d0f014ea7362) Thanks [@bluwy](https://github.com/bluwy)! - Disables the WebSocket server when creating a Vite server for loading config files diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts index 8a61150c7..060b25b8f 100644 --- a/packages/astro/client.d.ts +++ b/packages/astro/client.d.ts @@ -145,6 +145,9 @@ declare module 'astro:transitions/client' { import('./dist/virtual-modules/transitions-events.js').TransitionBeforeSwapEvent; export const isTransitionBeforePreparationEvent: EventModule['isTransitionBeforePreparationEvent']; export const isTransitionBeforeSwapEvent: EventModule['isTransitionBeforeSwapEvent']; + type TransitionSwapFunctionModule = + typeof import('./dist/virtual-modules/transitions-swap-functions.js'); + export const swapFunctions: TransitionSwapFunctionModule['swapFunctions']; } declare module 'astro:prefetch' { diff --git a/packages/astro/e2e/actions-blog.test.js b/packages/astro/e2e/actions-blog.test.js index e3a8c7cf8..d9c1bc1df 100644 --- a/packages/astro/e2e/actions-blog.test.js +++ b/packages/astro/e2e/actions-blog.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { testFactory } from './test-utils.js'; +import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/actions-blog/' }); +const test = testFactory(import.meta.url, { root: './fixtures/actions-blog/' }); let devServer; @@ -23,6 +23,7 @@ test.describe('Astro Actions - Blog', () => { await page.goto(astro.resolveUrl('/blog/first-post/')); const likeButton = page.getByLabel('Like'); + await waitForHydrate(page, likeButton); await expect(likeButton, 'like button starts with 10 likes').toContainText('10'); await likeButton.click(); await expect(likeButton, 'like button should increment likes').toContainText('11'); @@ -125,4 +126,13 @@ test.describe('Astro Actions - Blog', () => { await expect(comments).toBeVisible(); await expect(comments).toContainText(body); }); + + test('Logout action redirects', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/blog/first-post/')); + + const logoutButton = page.getByTestId('logout-button'); + await waitForHydrate(page, logoutButton); + await logoutButton.click(); + await expect(page).toHaveURL(astro.resolveUrl('/blog/')); + }); }); diff --git a/packages/astro/e2e/actions-react-19.test.js b/packages/astro/e2e/actions-react-19.test.js index 5ce72a419..3298db1e3 100644 --- a/packages/astro/e2e/actions-react-19.test.js +++ b/packages/astro/e2e/actions-react-19.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { testFactory } from './test-utils.js'; +import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/actions-react-19/' }); +const test = testFactory(import.meta.url, { root: './fixtures/actions-react-19/' }); let devServer; @@ -23,10 +23,12 @@ test.describe('Astro Actions - React 19', () => { await page.goto(astro.resolveUrl('/blog/first-post/')); const likeButton = page.getByLabel('likes-client'); + await waitForHydrate(page, likeButton); + 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 }); + await expect(likeButton).not.toBeDisabled(); }); test('Like action - server progressive enhancement', async ({ page, astro }) => { @@ -43,6 +45,8 @@ test.describe('Astro Actions - React 19', () => { await page.goto(astro.resolveUrl('/blog/first-post/')); const likeButton = page.getByLabel('likes-action-client'); + await waitForHydrate(page, likeButton); + await expect(likeButton).toBeVisible(); await likeButton.click(); diff --git a/packages/astro/e2e/astro-component.test.js b/packages/astro/e2e/astro-component.test.js index b77cbffa1..3a3c738b3 100644 --- a/packages/astro/e2e/astro-component.test.js +++ b/packages/astro/e2e/astro-component.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ root: './fixtures/astro-component/' }); +const test = testFactory(import.meta.url, { root: './fixtures/astro-component/' }); let devServer; diff --git a/packages/astro/e2e/astro-envs.test.js b/packages/astro/e2e/astro-envs.test.js index f6f3c5031..60baa65a0 100644 --- a/packages/astro/e2e/astro-envs.test.js +++ b/packages/astro/e2e/astro-envs.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/astro-envs/', devToolbar: { enabled: false, diff --git a/packages/astro/e2e/client-idle-timeout.test.js b/packages/astro/e2e/client-idle-timeout.test.js new file mode 100644 index 000000000..a39870619 --- /dev/null +++ b/packages/astro/e2e/client-idle-timeout.test.js @@ -0,0 +1,33 @@ +import { expect } from '@playwright/test'; +import { testFactory, waitForHydrate } from './test-utils.js'; + +const test = testFactory(import.meta.url, { root: './fixtures/client-idle-timeout/' }); + +let devServer; + +test.beforeAll(async ({ astro }) => { + devServer = await astro.startDevServer(); +}); + +test.afterAll(async () => { + await devServer.stop(); +}); + +test.describe('Client idle timeout', () => { + test('React counter', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); + + const counter = page.locator('#react-counter'); + await expect(counter, 'component is visible').toBeVisible(); + + const count = counter.locator('pre'); + await expect(count, 'initial count is 0').toHaveText('0'); + + await waitForHydrate(page, counter); + + const inc = counter.locator('.increment'); + await inc.click(); + + await expect(count, 'count incremented by 1').toHaveText('1'); + }); +}); diff --git a/packages/astro/e2e/client-only.test.js b/packages/astro/e2e/client-only.test.js index 08c5fb3ac..62a05f8dc 100644 --- a/packages/astro/e2e/client-only.test.js +++ b/packages/astro/e2e/client-only.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ root: './fixtures/client-only/' }); +const test = testFactory(import.meta.url, { root: './fixtures/client-only/' }); let devServer; diff --git a/packages/astro/e2e/content-collections.test.js b/packages/astro/e2e/content-collections.test.js index 63c5077c9..fdb8d5e00 100644 --- a/packages/astro/e2e/content-collections.test.js +++ b/packages/astro/e2e/content-collections.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ root: './fixtures/content-collections/' }); +const test = testFactory(import.meta.url, { root: './fixtures/content-collections/' }); let devServer; diff --git a/packages/astro/e2e/css.test.js b/packages/astro/e2e/css.test.js index fd4de700e..f865969f7 100644 --- a/packages/astro/e2e/css.test.js +++ b/packages/astro/e2e/css.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/css/', devToolbar: { enabled: false, diff --git a/packages/astro/e2e/custom-client-directives.test.js b/packages/astro/e2e/custom-client-directives.test.js index ef9de808a..8f90916f2 100644 --- a/packages/astro/e2e/custom-client-directives.test.js +++ b/packages/astro/e2e/custom-client-directives.test.js @@ -2,7 +2,7 @@ import { expect } from '@playwright/test'; import testAdapter from '../test/test-adapter.js'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/custom-client-directives/', }); diff --git a/packages/astro/e2e/dev-toolbar-audits.test.js b/packages/astro/e2e/dev-toolbar-audits.test.js index 6ef63cc1e..d0c5da847 100644 --- a/packages/astro/e2e/dev-toolbar-audits.test.js +++ b/packages/astro/e2e/dev-toolbar-audits.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/dev-toolbar/', }); diff --git a/packages/astro/e2e/dev-toolbar.test.js b/packages/astro/e2e/dev-toolbar.test.js index ae8b6ef5c..628b3af3a 100644 --- a/packages/astro/e2e/dev-toolbar.test.js +++ b/packages/astro/e2e/dev-toolbar.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/dev-toolbar/', }); diff --git a/packages/astro/e2e/error-cyclic.test.js b/packages/astro/e2e/error-cyclic.test.js index 84f4d1d1d..62b502fab 100644 --- a/packages/astro/e2e/error-cyclic.test.js +++ b/packages/astro/e2e/error-cyclic.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { getErrorOverlayContent, testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/error-cyclic/', }); diff --git a/packages/astro/e2e/error-sass.test.js b/packages/astro/e2e/error-sass.test.js index 11862fb86..05774220e 100644 --- a/packages/astro/e2e/error-sass.test.js +++ b/packages/astro/e2e/error-sass.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { getErrorOverlayContent, testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/error-sass/', }); diff --git a/packages/astro/e2e/errors.test.js b/packages/astro/e2e/errors.test.js index 34cecd816..f64a22b5c 100644 --- a/packages/astro/e2e/errors.test.js +++ b/packages/astro/e2e/errors.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { getErrorOverlayContent, testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/errors/', // Only test the error overlay, don't print to console vite: { diff --git a/packages/astro/e2e/fixtures/actions-blog/package.json b/packages/astro/e2e/fixtures/actions-blog/package.json index 04685b810..545ae2d37 100644 --- a/packages/astro/e2e/fixtures/actions-blog/package.json +++ b/packages/astro/e2e/fixtures/actions-blog/package.json @@ -14,7 +14,7 @@ "@astrojs/db": "workspace:*", "@astrojs/node": "workspace:*", "@astrojs/react": "workspace:*", - "@types/react": "^18.3.3", + "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "astro": "workspace:*", "react": "^18.3.1", diff --git a/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts b/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts index 0588f626c..7b640be51 100644 --- a/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts +++ b/packages/astro/e2e/fixtures/actions-blog/src/actions/index.ts @@ -3,11 +3,16 @@ import { ActionError, defineAction, z } from 'astro:actions'; import { getCollection } from 'astro:content'; export const server = { + logout: defineAction({ + handler: async () => { + await new Promise((r) => setTimeout(r, 500)); + }, + }), blog: { like: defineAction({ input: z.object({ postId: z.string() }), handler: async ({ postId }) => { - await new Promise((r) => setTimeout(r, 1000)); + await new Promise((r) => setTimeout(r, 500)); const { likes } = await db .update(Likes) @@ -30,7 +35,7 @@ export const server = { body: z.string().min(10), }), handler: async ({ postId, author, body }) => { - if (!(await getCollection('blog')).find(b => b.id === postId)) { + if (!(await getCollection('blog')).find((b) => b.id === postId)) { throw new ActionError({ code: 'NOT_FOUND', message: 'Post not found', diff --git a/packages/astro/e2e/fixtures/actions-blog/src/components/Logout.tsx b/packages/astro/e2e/fixtures/actions-blog/src/components/Logout.tsx new file mode 100644 index 000000000..737718d91 --- /dev/null +++ b/packages/astro/e2e/fixtures/actions-blog/src/components/Logout.tsx @@ -0,0 +1,16 @@ +import { actions } from 'astro:actions'; +import { navigate } from 'astro:transitions/client'; + +export function Logout() { + return ( + <button + data-testid="logout-button" + onClick={async () => { + const { error } = await actions.logout(); + if (!error) navigate('/blog/'); + }} + > + Logout + </button> + ); +} diff --git a/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro b/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro index fe97a8de1..ad4aea521 100644 --- a/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro +++ b/packages/astro/e2e/fixtures/actions-blog/src/pages/blog/[...slug].astro @@ -1,6 +1,7 @@ --- import { type CollectionEntry, getCollection, getEntry } from 'astro:content'; import BlogPost from '../../layouts/BlogPost.astro'; +import { Logout } from '../../components/Logout'; import { db, eq, Comment, Likes } from 'astro:db'; import { Like } from '../../components/Like'; import { PostComment } from '../../components/PostComment'; @@ -17,14 +18,13 @@ 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 Astro.callAction(actions.blog.like.orThrow, {postId: post.id}); + await Astro.callAction(actions.blog.like.orThrow, { postId: post.id }); } const comment = Astro.getActionResult(actions.blog.comment); @@ -40,6 +40,8 @@ const commentPostIdOverride = Astro.url.searchParams.get('commentPostIdOverride' <BlogPost {...post.data}> <Like postId={post.id} initial={initialLikes?.likes ?? 0} client:load /> + <Logout client:load /> + <form> <input type="hidden" name="like" /> <button type="submit" aria-label="get-request">Like GET request</button> @@ -57,17 +59,17 @@ const commentPostIdOverride = Astro.url.searchParams.get('commentPostIdOverride' /> <form method="POST" data-testid="progressive-fallback" action={actions.blog.comment.queryString}> <input type="hidden" name="postId" value={post.id} /> - <label for="fallback-author"> - Author - </label> - <input id="fallback-author" type="text" name="author" required /> - <label for="fallback-body" class="sr-only"> - Comment - </label> + <label for="fallback-author"> Author </label> + <input id="fallback-author" type="text" name="author" required /> + <label for="fallback-body" class="sr-only"> Comment </label> <textarea id="fallback-body" rows={10} name="body" required></textarea> - {isInputError(comment?.error) && comment.error.fields.body && ( - <p class="error" data-error="body">{comment.error.fields.body.toString()}</p> - )} + { + isInputError(comment?.error) && comment.error.fields.body && ( + <p class="error" data-error="body"> + {comment.error.fields.body.toString()} + </p> + ) + } <button type="submit">Post Comment</button> </form> <div data-testid="server-comments"> diff --git a/packages/astro/e2e/fixtures/client-idle-timeout/astro.config.mjs b/packages/astro/e2e/fixtures/client-idle-timeout/astro.config.mjs new file mode 100644 index 000000000..02dccb978 --- /dev/null +++ b/packages/astro/e2e/fixtures/client-idle-timeout/astro.config.mjs @@ -0,0 +1,9 @@ +import react from '@astrojs/react'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [ + react(), + ], +}); diff --git a/packages/astro/e2e/fixtures/client-idle-timeout/package.json b/packages/astro/e2e/fixtures/client-idle-timeout/package.json new file mode 100644 index 000000000..af4c41605 --- /dev/null +++ b/packages/astro/e2e/fixtures/client-idle-timeout/package.json @@ -0,0 +1,13 @@ +{ + "name": "@e2e/client-idle-timeout", + "version": "0.0.0", + "private": true, + "devDependencies": { + "@astrojs/react": "workspace:*", + "astro": "workspace:*" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +} diff --git a/packages/astro/e2e/fixtures/client-idle-timeout/src/components/Counter.jsx b/packages/astro/e2e/fixtures/client-idle-timeout/src/components/Counter.jsx new file mode 100644 index 000000000..9d2212b0c --- /dev/null +++ b/packages/astro/e2e/fixtures/client-idle-timeout/src/components/Counter.jsx @@ -0,0 +1,18 @@ +import React, { useState } from 'react'; + +export default function Counter({ children, count: initialCount = 0, id }) { + const [count, setCount] = useState(initialCount); + const add = () => setCount((i) => i + 1); + const subtract = () => setCount((i) => i - 1); + + return ( + <> + <div id={id} className="counter"> + <button className="decrement" onClick={subtract}>-</button> + <pre>{count}</pre> + <button className="increment" onClick={add}>+</button> + </div> + <div className="counter-message">{children}</div> + </> + ); +} diff --git a/packages/astro/e2e/fixtures/client-idle-timeout/src/pages/index.astro b/packages/astro/e2e/fixtures/client-idle-timeout/src/pages/index.astro new file mode 100644 index 000000000..0045ca55c --- /dev/null +++ b/packages/astro/e2e/fixtures/client-idle-timeout/src/pages/index.astro @@ -0,0 +1,16 @@ +--- +import Counter from '../components/Counter.jsx'; +--- + +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width" /> + <link rel="icon" type="image/x-icon" href="/favicon.ico" /> + </head> + <body> + <main> + <Counter id="react-counter" client:idle={{timeout: 200}}></Counter> + </main> + </body> +</html> diff --git a/packages/astro/e2e/fixtures/client-only/package.json b/packages/astro/e2e/fixtures/client-only/package.json index 4c9903c7b..d89fe7898 100644 --- a/packages/astro/e2e/fixtures/client-only/package.json +++ b/packages/astro/e2e/fixtures/client-only/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/errors/package.json b/packages/astro/e2e/fixtures/errors/package.json index 1d01a376f..ab47d7da8 100644 --- a/packages/astro/e2e/fixtures/errors/package.json +++ b/packages/astro/e2e/fixtures/errors/package.json @@ -13,8 +13,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "sass": "^1.77.8", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/multiple-frameworks/package.json b/packages/astro/e2e/fixtures/multiple-frameworks/package.json index 810e503f7..f90d9725d 100644 --- a/packages/astro/e2e/fixtures/multiple-frameworks/package.json +++ b/packages/astro/e2e/fixtures/multiple-frameworks/package.json @@ -16,8 +16,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/nested-in-preact/package.json b/packages/astro/e2e/fixtures/nested-in-preact/package.json index 339cdcbc1..c5f10cd43 100644 --- a/packages/astro/e2e/fixtures/nested-in-preact/package.json +++ b/packages/astro/e2e/fixtures/nested-in-preact/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/nested-in-react/package.json b/packages/astro/e2e/fixtures/nested-in-react/package.json index 9573ef266..bac0d1bae 100644 --- a/packages/astro/e2e/fixtures/nested-in-react/package.json +++ b/packages/astro/e2e/fixtures/nested-in-react/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/nested-in-solid/package.json b/packages/astro/e2e/fixtures/nested-in-solid/package.json index 6e71432f0..4a2748eb5 100644 --- a/packages/astro/e2e/fixtures/nested-in-solid/package.json +++ b/packages/astro/e2e/fixtures/nested-in-solid/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/package.json b/packages/astro/e2e/fixtures/nested-in-svelte/package.json index 1f319d028..a98e441f5 100644 --- a/packages/astro/e2e/fixtures/nested-in-svelte/package.json +++ b/packages/astro/e2e/fixtures/nested-in-svelte/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/nested-in-vue/package.json b/packages/astro/e2e/fixtures/nested-in-vue/package.json index bd44589f3..1702f98bf 100644 --- a/packages/astro/e2e/fixtures/nested-in-vue/package.json +++ b/packages/astro/e2e/fixtures/nested-in-vue/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/nested-recursive/package.json b/packages/astro/e2e/fixtures/nested-recursive/package.json index ab809a3a3..d35cf0eb5 100644 --- a/packages/astro/e2e/fixtures/nested-recursive/package.json +++ b/packages/astro/e2e/fixtures/nested-recursive/package.json @@ -14,8 +14,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" }, "scripts": { diff --git a/packages/astro/e2e/fixtures/solid-circular/package.json b/packages/astro/e2e/fixtures/solid-circular/package.json index 21a0b66ba..aba997250 100644 --- a/packages/astro/e2e/fixtures/solid-circular/package.json +++ b/packages/astro/e2e/fixtures/solid-circular/package.json @@ -7,6 +7,6 @@ "astro": "workspace:*" }, "devDependencies": { - "solid-js": "^1.8.21" + "solid-js": "^1.8.22" } } diff --git a/packages/astro/e2e/fixtures/solid-circular/src/env.d.ts b/packages/astro/e2e/fixtures/solid-circular/src/env.d.ts deleted file mode 100644 index 8c34fb45e..000000000 --- a/packages/astro/e2e/fixtures/solid-circular/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="astro/client" />
\ No newline at end of file diff --git a/packages/astro/e2e/fixtures/solid-component/package.json b/packages/astro/e2e/fixtures/solid-component/package.json index 850a41587..86269a915 100644 --- a/packages/astro/e2e/fixtures/solid-component/package.json +++ b/packages/astro/e2e/fixtures/solid-component/package.json @@ -6,6 +6,6 @@ "@astrojs/mdx": "workspace:*", "@astrojs/solid-js": "workspace:*", "astro": "workspace:*", - "solid-js": "^1.8.21" + "solid-js": "^1.8.22" } } diff --git a/packages/astro/e2e/fixtures/solid-recurse/package.json b/packages/astro/e2e/fixtures/solid-recurse/package.json index aa51ebe5d..a7d97b39c 100644 --- a/packages/astro/e2e/fixtures/solid-recurse/package.json +++ b/packages/astro/e2e/fixtures/solid-recurse/package.json @@ -7,6 +7,6 @@ "astro": "workspace:*" }, "devDependencies": { - "solid-js": "^1.8.21" + "solid-js": "^1.8.22" } } diff --git a/packages/astro/e2e/fixtures/svelte-component/package.json b/packages/astro/e2e/fixtures/svelte-component/package.json index e5d238f74..04acc35e7 100644 --- a/packages/astro/e2e/fixtures/svelte-component/package.json +++ b/packages/astro/e2e/fixtures/svelte-component/package.json @@ -6,6 +6,6 @@ "@astrojs/mdx": "workspace:*", "@astrojs/svelte": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/e2e/fixtures/svelte-component/src/env.d.ts b/packages/astro/e2e/fixtures/svelte-component/src/env.d.ts deleted file mode 100644 index 8c34fb45e..000000000 --- a/packages/astro/e2e/fixtures/svelte-component/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="astro/client" />
\ No newline at end of file diff --git a/packages/astro/e2e/fixtures/view-transitions/package.json b/packages/astro/e2e/fixtures/view-transitions/package.json index b5460ebcd..0a0ad81f2 100644 --- a/packages/astro/e2e/fixtures/view-transitions/package.json +++ b/packages/astro/e2e/fixtures/view-transitions/package.json @@ -10,7 +10,7 @@ "astro": "workspace:*", "react": "^18.3.1", "react-dom": "^18.3.1", - "svelte": "^4.2.18", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/keep-style-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/keep-style-one.astro index 413a404d7..f71898d81 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/keep-style-one.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/keep-style-one.astro @@ -7,22 +7,22 @@ import Layout from '../components/Layout.astro'; </Layout> <script> - import { deselectScripts, saveFocus, swapBodyElement, swapHeadElements, swapRootAttributes } from '../../node_modules/astro/dist/transitions/swap-functions' + import { swapFunctions } from 'astro:transitions/client'; document.addEventListener('astro:before-swap', (e) => { e.swap = () => keepStyle(e.newDocument) }); function keepStyle(doc: Document) { - deselectScripts(doc); - swapRootAttributes(doc); + swapFunctions.deselectScripts(doc); + swapFunctions.swapRootAttributes(doc); { const dynamicStyle = document.head.querySelector('style:not(:empty)'); - swapHeadElements(doc); + swapFunctions.swapHeadElements(doc); dynamicStyle && document.head.insertAdjacentElement('afterbegin', dynamicStyle); } - const restoreFocusFunction = saveFocus(); - swapBodyElement(doc.body, document.body) + const restoreFocusFunction = swapFunctions.saveFocus(); + swapFunctions.swapBodyElement(doc.body, document.body) restoreFocusFunction(); } </script> diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/keep-theme-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/keep-theme-one.astro index a4c942d58..18ae0221f 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/keep-theme-one.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/keep-theme-one.astro @@ -6,18 +6,18 @@ import Layout from '../components/Layout.astro'; <a id="click" href="/keep-two">go to next page</a> </Layout> <script> - import { deselectScripts, saveFocus, swapBodyElement, swapHeadElements, swapRootAttributes } from '../../node_modules/astro/dist/transitions/swap-functions' + import { swapFunctions } from 'astro:transitions/client'; function keepTheme(doc:Document) { - deselectScripts(doc); + swapFunctions.deselectScripts(doc); { const theme = document.documentElement.getAttribute('data-theme')!; - swapRootAttributes(doc); + swapFunctions.swapRootAttributes(doc); document.documentElement.setAttribute('data-theme', theme); } - swapHeadElements(doc); - const restoreFocusFunction = saveFocus(); - swapBodyElement(doc.body, document.body) + swapFunctions.swapHeadElements(doc); + const restoreFocusFunction = swapFunctions.saveFocus(); + swapFunctions.swapBodyElement(doc.body, document.body) restoreFocusFunction(); } diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/replace-main-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/replace-main-one.astro index 1709661e8..c43ce78a9 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/replace-main-one.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/replace-main-one.astro @@ -9,24 +9,24 @@ import Layout from '../components/Layout.astro'; <a id="click" href="/keep-two">go to next page</a> </Layout> <script> - import { deselectScripts, saveFocus, swapBodyElement, swapHeadElements, swapRootAttributes } from "../../node_modules/astro/dist/transitions/swap-functions" + import { swapFunctions } from 'astro:transitions/client'; document.addEventListener('astro:before-swap', (e) => { e.swap = () => replaceMain(e.newDocument) }); function replaceMain(doc:Document){ - deselectScripts(doc); - swapRootAttributes(doc); - swapHeadElements(doc); - const restoreFocusFunction = saveFocus(); + swapFunctions.deselectScripts(doc); + swapFunctions.swapRootAttributes(doc); + swapFunctions.swapHeadElements(doc); + const restoreFocusFunction = swapFunctions.saveFocus(); { const newMain = doc.body.querySelector('main section'); const oldMain = document.body.querySelector('main section'); if (newMain && oldMain) { - swapBodyElement(newMain, oldMain); + swapFunctions.swapBodyElement(newMain, oldMain); } else { - swapBodyElement(doc.body, document.body); + swapFunctions.swapBodyElement(doc.body, document.body); } } restoreFocusFunction(); diff --git a/packages/astro/e2e/hmr.test.js b/packages/astro/e2e/hmr.test.js index 72eaf8375..1f0cda2c4 100644 --- a/packages/astro/e2e/hmr.test.js +++ b/packages/astro/e2e/hmr.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/hmr/', devToolbar: { enabled: false, diff --git a/packages/astro/e2e/hydration-race.test.js b/packages/astro/e2e/hydration-race.test.js index 0ee578243..95469fe73 100644 --- a/packages/astro/e2e/hydration-race.test.js +++ b/packages/astro/e2e/hydration-race.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/hydration-race/', }); diff --git a/packages/astro/e2e/i18n.test.js b/packages/astro/e2e/i18n.test.js index e7d74a551..88d3a0b08 100644 --- a/packages/astro/e2e/i18n.test.js +++ b/packages/astro/e2e/i18n.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/i18n/', devToolbar: { enabled: false, diff --git a/packages/astro/e2e/multiple-frameworks.test.js b/packages/astro/e2e/multiple-frameworks.test.js index 08a2fb648..4b52dfd4c 100644 --- a/packages/astro/e2e/multiple-frameworks.test.js +++ b/packages/astro/e2e/multiple-frameworks.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ root: './fixtures/multiple-frameworks/' }); +const test = testFactory(import.meta.url, { root: './fixtures/multiple-frameworks/' }); let devServer; diff --git a/packages/astro/e2e/namespaced-component.test.js b/packages/astro/e2e/namespaced-component.test.js index 58c00713a..1212e0ce1 100644 --- a/packages/astro/e2e/namespaced-component.test.js +++ b/packages/astro/e2e/namespaced-component.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/namespaced-component/', }); diff --git a/packages/astro/e2e/nested-in-preact.test.js b/packages/astro/e2e/nested-in-preact.test.js index f2bc4d728..2ad62e95f 100644 --- a/packages/astro/e2e/nested-in-preact.test.js +++ b/packages/astro/e2e/nested-in-preact.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/nested-in-preact/' }); +const test = testFactory(import.meta.url, { root: './fixtures/nested-in-preact/' }); let devServer; diff --git a/packages/astro/e2e/nested-in-react.test.js b/packages/astro/e2e/nested-in-react.test.js index db9eeb0dd..7dee69937 100644 --- a/packages/astro/e2e/nested-in-react.test.js +++ b/packages/astro/e2e/nested-in-react.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/nested-in-react/' }); +const test = testFactory(import.meta.url, { root: './fixtures/nested-in-react/' }); let devServer; diff --git a/packages/astro/e2e/nested-in-solid.test.js b/packages/astro/e2e/nested-in-solid.test.js index 0fab17468..2d9deade1 100644 --- a/packages/astro/e2e/nested-in-solid.test.js +++ b/packages/astro/e2e/nested-in-solid.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/nested-in-solid/' }); +const test = testFactory(import.meta.url, { root: './fixtures/nested-in-solid/' }); let devServer; diff --git a/packages/astro/e2e/nested-in-svelte.test.js b/packages/astro/e2e/nested-in-svelte.test.js index 88aa826a8..eeecb0442 100644 --- a/packages/astro/e2e/nested-in-svelte.test.js +++ b/packages/astro/e2e/nested-in-svelte.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/nested-in-svelte/' }); +const test = testFactory(import.meta.url, { root: './fixtures/nested-in-svelte/' }); let devServer; diff --git a/packages/astro/e2e/nested-in-vue.test.js b/packages/astro/e2e/nested-in-vue.test.js index deed309c7..7e25e4747 100644 --- a/packages/astro/e2e/nested-in-vue.test.js +++ b/packages/astro/e2e/nested-in-vue.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/nested-in-vue/' }); +const test = testFactory(import.meta.url, { root: './fixtures/nested-in-vue/' }); let devServer; diff --git a/packages/astro/e2e/nested-recursive.test.js b/packages/astro/e2e/nested-recursive.test.js index 262cd6772..d9f612642 100644 --- a/packages/astro/e2e/nested-recursive.test.js +++ b/packages/astro/e2e/nested-recursive.test.js @@ -3,7 +3,7 @@ import { loadFixture, waitForHydrate } from './test-utils.js'; const test = base.extend({ astro: async ({}, use) => { - const fixture = await loadFixture({ root: './fixtures/nested-recursive/' }); + const fixture = await loadFixture(import.meta.url, { root: './fixtures/nested-recursive/' }); await use(fixture); }, }); diff --git a/packages/astro/e2e/nested-styles.test.js b/packages/astro/e2e/nested-styles.test.js index c482570f0..1a3aeb0e0 100644 --- a/packages/astro/e2e/nested-styles.test.js +++ b/packages/astro/e2e/nested-styles.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/nested-styles/', devToolbar: { enabled: false, diff --git a/packages/astro/e2e/pass-js.test.js b/packages/astro/e2e/pass-js.test.js index 0db9895d1..91e3b5c5e 100644 --- a/packages/astro/e2e/pass-js.test.js +++ b/packages/astro/e2e/pass-js.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/pass-js/', }); diff --git a/packages/astro/e2e/preact-compat-component.test.js b/packages/astro/e2e/preact-compat-component.test.js index e1b603e7f..83831c9e7 100644 --- a/packages/astro/e2e/preact-compat-component.test.js +++ b/packages/astro/e2e/preact-compat-component.test.js @@ -1,6 +1,8 @@ import { prepareTestFactory } from './shared-component-tests.js'; -const { test, createTests } = prepareTestFactory({ root: './fixtures/preact-compat-component/' }); +const { test, createTests } = prepareTestFactory(import.meta.url, { + root: './fixtures/preact-compat-component/', +}); const config = { counterComponentFilePath: './src/components/Counter.jsx', diff --git a/packages/astro/e2e/preact-component.test.js b/packages/astro/e2e/preact-component.test.js index d808b4890..3beb14da3 100644 --- a/packages/astro/e2e/preact-component.test.js +++ b/packages/astro/e2e/preact-component.test.js @@ -1,6 +1,8 @@ import { prepareTestFactory } from './shared-component-tests.js'; -const { test, createTests } = prepareTestFactory({ root: './fixtures/preact-component/' }); +const { test, createTests } = prepareTestFactory(import.meta.url, { + root: './fixtures/preact-component/', +}); const config = { counterComponentFilePath: './src/components/Counter.jsx', diff --git a/packages/astro/e2e/preact-lazy-component.test.js b/packages/astro/e2e/preact-lazy-component.test.js index 585d2d347..ce0e3afec 100644 --- a/packages/astro/e2e/preact-lazy-component.test.js +++ b/packages/astro/e2e/preact-lazy-component.test.js @@ -1,6 +1,8 @@ import { prepareTestFactory } from './shared-component-tests.js'; -const { test, createTests } = prepareTestFactory({ root: './fixtures/preact-lazy-component/' }); +const { test, createTests } = prepareTestFactory(import.meta.url, { + root: './fixtures/preact-lazy-component/', +}); const config = { counterComponentFilePath: './src/components/Counter.jsx', diff --git a/packages/astro/e2e/prefetch.test.js b/packages/astro/e2e/prefetch.test.js index 84ead590c..2da44189a 100644 --- a/packages/astro/e2e/prefetch.test.js +++ b/packages/astro/e2e/prefetch.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ +const test = testFactory(import.meta.url, { root: './fixtures/prefetch/', }); diff --git a/packages/astro/e2e/react-component.test.js b/packages/astro/e2e/react-component.test.js index 361ee8d69..4585887ad 100644 --- a/packages/astro/e2e/react-component.test.js +++ b/packages/astro/e2e/react-component.test.js @@ -1,7 +1,9 @@ import { expect } from '@playwright/test'; import { prepareTestFactory } from './shared-component-tests.js'; -const { test, createTests } = prepareTestFactory({ root: './fixtures/react-component/' }); +const { test, createTests } = prepareTestFactory(import.meta.url, { + root: './fixtures/react-component/', +}); const config = { counterComponentFilePath: './src/components/Counter.jsx', diff --git a/packages/astro/e2e/server-islands.test.js b/packages/astro/e2e/server-islands.test.js index b45a58cd8..24ac7b918 100644 --- a/packages/astro/e2e/server-islands.test.js +++ b/packages/astro/e2e/server-islands.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ root: './fixtures/server-islands/' }); +const test = testFactory(import.meta.url, { root: './fixtures/server-islands/' }); test.describe('Server islands', () => { test.describe('Development', () => { diff --git a/packages/astro/e2e/shared-component-tests.js b/packages/astro/e2e/shared-component-tests.js index 024f1aade..ea45da13b 100644 --- a/packages/astro/e2e/shared-component-tests.js +++ b/packages/astro/e2e/shared-component-tests.js @@ -1,8 +1,8 @@ import { expect } from '@playwright/test'; import { scrollToElement, testFactory, waitForHydrate } from './test-utils.js'; -export function prepareTestFactory(opts, { canReplayClicks = false } = {}) { - const test = testFactory(opts); +export function prepareTestFactory(testFile, opts, { canReplayClicks = false } = {}) { + const test = testFactory(testFile, opts); let devServer; @@ -120,6 +120,7 @@ export function prepareTestFactory(opts, { canReplayClicks = false } = {}) { await page.goto(astro.resolveUrl(pageUrl)); const label = page.locator('#client-only'); + await waitForHydrate(page, label); await expect(label, 'component is visible').toBeVisible(); await expect(label, 'slot text is visible').toHaveText('Framework client:only component'); diff --git a/packages/astro/e2e/solid-circular.test.js b/packages/astro/e2e/solid-circular.test.js index 5dd0e8b80..796800a21 100644 --- a/packages/astro/e2e/solid-circular.test.js +++ b/packages/astro/e2e/solid-circular.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ root: './fixtures/solid-circular/' }); +const test = testFactory(import.meta.url, { root: './fixtures/solid-circular/' }); let devServer; diff --git a/packages/astro/e2e/solid-component.test.js b/packages/astro/e2e/solid-component.test.js index b998d1873..6a934b1c4 100644 --- a/packages/astro/e2e/solid-component.test.js +++ b/packages/astro/e2e/solid-component.test.js @@ -1,6 +1,7 @@ import { prepareTestFactory } from './shared-component-tests.js'; const { test, createTests } = prepareTestFactory( + import.meta.url, { root: './fixtures/solid-component/' }, { canReplayClicks: true, diff --git a/packages/astro/e2e/solid-recurse.test.js b/packages/astro/e2e/solid-recurse.test.js index de3759e98..eb0fa3770 100644 --- a/packages/astro/e2e/solid-recurse.test.js +++ b/packages/astro/e2e/solid-recurse.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/solid-recurse/' }); +const test = testFactory(import.meta.url, { root: './fixtures/solid-recurse/' }); let devServer; diff --git a/packages/astro/e2e/svelte-component.test.js b/packages/astro/e2e/svelte-component.test.js index 01c7aa41d..d72235797 100644 --- a/packages/astro/e2e/svelte-component.test.js +++ b/packages/astro/e2e/svelte-component.test.js @@ -2,7 +2,9 @@ import { expect } from '@playwright/test'; import { prepareTestFactory } from './shared-component-tests.js'; import { waitForHydrate } from './test-utils.js'; -const { test, createTests } = prepareTestFactory({ root: './fixtures/svelte-component/' }); +const { test, createTests } = prepareTestFactory(import.meta.url, { + root: './fixtures/svelte-component/', +}); const config = { componentFilePath: './src/components/SvelteComponent.svelte', diff --git a/packages/astro/e2e/tailwindcss.test.js b/packages/astro/e2e/tailwindcss.test.js index e58e10dfd..c86d01b0e 100644 --- a/packages/astro/e2e/tailwindcss.test.js +++ b/packages/astro/e2e/tailwindcss.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory } from './test-utils.js'; -const test = testFactory({ root: './fixtures/tailwindcss/' }); +const test = testFactory(import.meta.url, { root: './fixtures/tailwindcss/' }); let devServer; diff --git a/packages/astro/e2e/test-utils.js b/packages/astro/e2e/test-utils.js index 48fcd17bd..933186a71 100644 --- a/packages/astro/e2e/test-utils.js +++ b/packages/astro/e2e/test-utils.js @@ -14,30 +14,40 @@ const testFileToPort = new Map(); for (let i = 0; i < testFiles.length; i++) { const file = testFiles[i]; if (file.endsWith('.test.js')) { - testFileToPort.set(file.slice(0, -8), 4000 + i); + testFileToPort.set(file, 4000 + i); } } -export function loadFixture(inlineConfig) { +export function loadFixture(testFile, inlineConfig) { if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); + const port = testFileToPort.get(path.basename(testFile)); + // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath // without this, the main `loadFixture` helper will resolve relative to `packages/astro/test` return baseLoadFixture({ ...inlineConfig, root: fileURLToPath(new URL(inlineConfig.root, import.meta.url)), server: { - port: testFileToPort.get(path.basename(inlineConfig.root)), + ...inlineConfig?.server, + port, + }, + vite: { + ...inlineConfig?.vite, + server: { + ...inlineConfig?.vite?.server, + strictPort: true, + }, }, }); } -export function testFactory(inlineConfig) { +export function testFactory(testFile, inlineConfig) { let fixture; const test = testBase.extend({ astro: async ({}, use) => { - fixture = fixture || (await loadFixture(inlineConfig)); + fixture = fixture || (await loadFixture(testFile, inlineConfig)); await use(fixture); }, }); diff --git a/packages/astro/e2e/ts-resolution.test.js b/packages/astro/e2e/ts-resolution.test.js index d2d3fcfe8..256269542 100644 --- a/packages/astro/e2e/ts-resolution.test.js +++ b/packages/astro/e2e/ts-resolution.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/ts-resolution/' }); +const test = testFactory(import.meta.url, { root: './fixtures/ts-resolution/' }); function runTest(it) { it('client:idle', async ({ page, astro }) => { diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index 135ec5571..181994cfa 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { testFactory, waitForHydrate } from './test-utils.js'; -const test = testFactory({ root: './fixtures/view-transitions/' }); +const test = testFactory(import.meta.url, { root: './fixtures/view-transitions/' }); let devServer; @@ -447,7 +447,9 @@ test.describe('View Transitions', () => { expect(consoleCount).toEqual(1); // forward '' to 'hash' (no transition) - await page.goForward(); + // NOTE: the networkidle below is needed for Firefox to consistently + // pass the `#longpage` viewport check below + await page.goForward({ waitUntil: 'networkidle' }); locator = page.locator('#click-one-again'); await expect(locator).toBeInViewport(); expect(consoleCount).toEqual(1); @@ -1445,7 +1447,7 @@ test.describe('View Transitions', () => { await page.click('#click'); await expect(page.locator('#name'), 'should have content').toHaveText('Keep 2'); - const styleElement = await page.$('head > style'); + const styleElement = await page.$('head > style:nth-child(1)'); const styleContent = await page.evaluate((style) => style.innerHTML, styleElement); expect(styleContent).toBe('body { background-color: purple; }'); }); diff --git a/packages/astro/e2e/vue-component.test.js b/packages/astro/e2e/vue-component.test.js index 3be31af85..034fe5a6e 100644 --- a/packages/astro/e2e/vue-component.test.js +++ b/packages/astro/e2e/vue-component.test.js @@ -1,6 +1,8 @@ import { expect } from '@playwright/test'; import { prepareTestFactory } from './shared-component-tests.js'; -const { test, createTests } = prepareTestFactory({ root: './fixtures/vue-component/' }); +const { test, createTests } = prepareTestFactory(import.meta.url, { + root: './fixtures/vue-component/', +}); const config = { componentFilePath: './src/components/VueComponent.vue', diff --git a/packages/astro/package.json b/packages/astro/package.json index c15d9aba2..a448146ee 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -125,10 +125,9 @@ "@astrojs/internal-helpers": "workspace:*", "@astrojs/markdown-remark": "workspace:*", "@astrojs/telemetry": "workspace:*", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/traverse": "^7.25.3", - "@babel/types": "^7.25.2", + "@babel/core": "^7.25.2", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/types": "^7.25.4", "@oslojs/encoding": "^0.4.1", "@rollup/pluginutils": "^5.1.0", "@types/cookie": "^0.6.0", @@ -150,7 +149,6 @@ "es-module-lexer": "^1.5.4", "esbuild": "^0.21.5", "estree-walker": "^3.0.3", - "execa": "^8.0.1", "fast-glob": "^3.3.2", "flattie": "^1.1.1", "github-slugger": "^2.0.0", @@ -160,10 +158,11 @@ "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.11", - "micromatch": "^4.0.7", + "magicast": "^0.3.5", + "micromatch": "^4.0.8", "mrmime": "^2.0.0", "neotraverse": "^0.6.18", - "ora": "^8.0.1", + "ora": "^8.1.0", "p-limit": "^6.1.0", "p-queue": "^8.0.1", "path-to-regexp": "^6.2.2", @@ -174,10 +173,11 @@ "shiki": "^1.14.1", "string-width": "^7.2.0", "strip-ansi": "^7.1.0", + "tinyexec": "^0.3.0", "tsconfck": "^3.1.1", "unist-util-visit": "^5.0.0", - "vfile": "^6.0.2", - "vite": "^5.4.1", + "vfile": "^6.0.3", + "vite": "^5.4.2", "vitefu": "^0.2.5", "which-pm": "^3.0.0", "xxhash-wasm": "^1.0.2", @@ -212,9 +212,10 @@ "astro-scripts": "workspace:*", "cheerio": "1.0.0", "eol": "^0.9.1", - "expect-type": "^0.19.0", + "execa": "^8.0.1", + "expect-type": "^0.20.0", "mdast-util-mdx": "^3.0.0", - "mdast-util-mdx-jsx": "^3.1.2", + "mdast-util-mdx-jsx": "^3.1.3", "memfs": "^4.11.1", "node-mocks-http": "^1.15.1", "parse-srcset": "^1.0.2", @@ -222,9 +223,9 @@ "rehype-slug": "^6.0.0", "rehype-toc": "^3.0.2", "remark-code-titles": "^0.1.2", - "rollup": "^4.21.0", + "rollup": "^4.21.1", "sass": "^1.77.8", - "undici": "^6.19.7", + "undici": "^6.19.8", "unified": "^11.0.5" }, "engines": { diff --git a/packages/astro/performance/fixtures/md/package.json b/packages/astro/performance/fixtures/md/package.json index d51beb903..90db727e4 100644 --- a/packages/astro/performance/fixtures/md/package.json +++ b/packages/astro/performance/fixtures/md/package.json @@ -16,7 +16,7 @@ "dependencies": { "@astrojs/react": "workspace:*", "@performance/utils": "workspace:*", - "@types/react": "^18.3.3", + "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "astro": "workspace:*", "react": "^18.3.1", diff --git a/packages/astro/performance/fixtures/mdoc/package.json b/packages/astro/performance/fixtures/mdoc/package.json index 406c9b1d4..5bcd443ae 100644 --- a/packages/astro/performance/fixtures/mdoc/package.json +++ b/packages/astro/performance/fixtures/mdoc/package.json @@ -17,7 +17,7 @@ "@astrojs/markdoc": "workspace:*", "@astrojs/react": "workspace:*", "@performance/utils": "workspace:*", - "@types/react": "^18.3.3", + "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "astro": "workspace:*", "react": "^18.3.1", diff --git a/packages/astro/performance/fixtures/mdoc/src/env.d.ts b/packages/astro/performance/fixtures/mdoc/src/env.d.ts deleted file mode 100644 index acef35f17..000000000 --- a/packages/astro/performance/fixtures/mdoc/src/env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// <reference path="../.astro/types.d.ts" /> -/// <reference types="astro/client" /> diff --git a/packages/astro/performance/fixtures/mdx/package.json b/packages/astro/performance/fixtures/mdx/package.json index 973bfd530..e1fc69d0d 100644 --- a/packages/astro/performance/fixtures/mdx/package.json +++ b/packages/astro/performance/fixtures/mdx/package.json @@ -17,7 +17,7 @@ "@astrojs/mdx": "workspace:*", "@astrojs/react": "workspace:*", "@performance/utils": "workspace:*", - "@types/react": "^18.3.3", + "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "astro": "workspace:*", "react": "^18.3.1", diff --git a/packages/astro/performance/fixtures/mdx/src/env.d.ts b/packages/astro/performance/fixtures/mdx/src/env.d.ts deleted file mode 100644 index acef35f17..000000000 --- a/packages/astro/performance/fixtures/mdx/src/env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// <reference path="../.astro/types.d.ts" /> -/// <reference types="astro/client" /> diff --git a/packages/astro/playwright.config.js b/packages/astro/playwright.config.js index 5aacd6d01..26572c66c 100644 --- a/packages/astro/playwright.config.js +++ b/packages/astro/playwright.config.js @@ -1,4 +1,5 @@ import { defineConfig } from '@playwright/test'; + // NOTE: Sometimes, tests fail with `TypeError: process.stdout.clearLine is not a function` // for some reason. This comes from Vite, and is conditionally called based on `isTTY`. // We set it to false here to skip this odd behavior. @@ -6,35 +7,16 @@ process.stdout.isTTY = false; export default defineConfig({ testMatch: 'e2e/*.test.js', - /* Maximum time one test can run for. */ - timeout: 40 * 1000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 4 * 1000, - }, - /* Fail the build on CI if you accidentally left test in the source code. */ + timeout: 40000, forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 3 : 0, - /* Opt out of parallel tests on CI. */ - workers: 1, - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, projects: [ { name: 'Chrome Stable', use: { browserName: 'chromium', channel: 'chrome', - args: ['--use-gl=egl'], }, }, ], diff --git a/packages/astro/playwright.firefox.config.js b/packages/astro/playwright.firefox.config.js index 537bb4099..00b82d999 100644 --- a/packages/astro/playwright.firefox.config.js +++ b/packages/astro/playwright.firefox.config.js @@ -1,43 +1,24 @@ +import { defineConfig } from '@playwright/test'; + // NOTE: Sometimes, tests fail with `TypeError: process.stdout.clearLine is not a function` // for some reason. This comes from Vite, and is conditionally called based on `isTTY`. // We set it to false here to skip this odd behavior. process.stdout.isTTY = false; -const config = { +export default defineConfig({ // TODO: add more tests like view transitions and audits, and fix them. Some of them are failing. testMatch: ['e2e/css.test.js', 'e2e/prefetch.test.js', 'e2e/view-transitions.test.js'], - /* Maximum time one test can run for. */ - timeout: 40 * 1000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 4 * 1000, - }, - /* Fail the build on CI if you accidentally left test in the source code. */ + timeout: 40000, forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 3 : 0, - /* Opt out of parallel tests on CI. */ - workers: 1, - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, projects: [ { name: 'Firefox Stable', use: { browserName: 'firefox', channel: 'firefox', - args: ['--use-gl=egl'], }, }, ], -}; - -export default config; +}); diff --git a/packages/astro/src/actions/runtime/middleware.ts b/packages/astro/src/actions/runtime/middleware.ts index b6f3221b5..b7cb4861c 100644 --- a/packages/astro/src/actions/runtime/middleware.ts +++ b/packages/astro/src/actions/runtime/middleware.ts @@ -23,8 +23,18 @@ export type Locals = { }; export const onRequest = defineMiddleware(async (context, next) => { + if ((context as any)._isPrerendered) { + if (context.request.method === 'POST') { + // eslint-disable-next-line no-console + console.warn( + yellow('[astro:actions]'), + 'POST requests should not be sent to prerendered pages. If you\'re using Actions, disable prerendering with `export const prerender = "false".', + ); + } + return next(); + } + const locals = context.locals as Locals; - const { request } = 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 // `_actionPayload` is the same for every page, @@ -39,16 +49,6 @@ export const onRequest = defineMiddleware(async (context, next) => { return renderResult({ context, next, ...actionPayload }); } - // Heuristic: If body is null, Astro might've reset this for prerendering. - if (import.meta.env.DEV && request.method === 'POST' && request.body === null) { - // eslint-disable-next-line no-console - console.warn( - yellow('[astro:actions]'), - 'POST requests should not be sent to prerendered pages. If you\'re using Actions, disable prerendering with `export const prerender = "false".', - ); - return next(); - } - const actionName = context.url.searchParams.get(ACTION_QUERY_PARAMS.actionName); if (context.request.method === 'POST' && actionName) { @@ -93,7 +93,11 @@ async function handlePost({ context, next, actionName, -}: { context: APIContext; next: MiddlewareNext; actionName: string }) { +}: { + context: APIContext; + next: MiddlewareNext; + actionName: string; +}) { const { request } = context; const baseAction = await getAction(actionName); diff --git a/packages/astro/src/actions/runtime/utils.ts b/packages/astro/src/actions/runtime/utils.ts index 199809d4e..d8b339a09 100644 --- a/packages/astro/src/actions/runtime/utils.ts +++ b/packages/astro/src/actions/runtime/utils.ts @@ -10,7 +10,10 @@ export function hasContentType(contentType: string, expected: string[]) { return expected.some((t) => type === t); } -export type ActionAPIContext = Omit<APIContext, 'getActionResult' | 'callAction' | 'props'>; +export type ActionAPIContext = Omit< + APIContext, + 'getActionResult' | 'callAction' | 'props' | 'redirect' +>; export type MaybePromise<T> = T | Promise<T>; /** diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts index 9bc387d6b..fcb0dc603 100644 --- a/packages/astro/src/actions/runtime/virtual/server.ts +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -9,9 +9,6 @@ export * from './shared.js'; export { z } from 'zod'; export type ActionAccept = 'form' | 'json'; -export type ActionInputSchema<T extends ActionAccept | undefined> = T extends 'form' - ? z.AnyZodObject | z.ZodType<FormData> - : z.ZodType; export type ActionHandler<TInputSchema, TOutput> = TInputSchema extends z.ZodType ? (input: z.infer<TInputSchema>, context: ActionAPIContext) => MaybePromise<TOutput> @@ -22,7 +19,7 @@ export type ActionReturnType<T extends ActionHandler<any, any>> = Awaited<Return export type ActionClient< TOutput, TAccept extends ActionAccept | undefined, - TInputSchema extends ActionInputSchema<TAccept> | undefined, + TInputSchema extends z.ZodType | undefined, > = TInputSchema extends z.ZodType ? (( input: TAccept extends 'form' ? FormData : z.input<TInputSchema>, @@ -46,7 +43,7 @@ export type ActionClient< export function defineAction< TOutput, TAccept extends ActionAccept | undefined = undefined, - TInputSchema extends ActionInputSchema<ActionAccept> | undefined = TAccept extends 'form' + TInputSchema extends z.ZodType | undefined = TAccept extends 'form' ? // If `input` is omitted, default to `FormData` for forms and `any` for JSON. z.ZodType<FormData> : undefined, @@ -83,7 +80,7 @@ export function defineAction< return safeServerHandler as ActionClient<TOutput, TAccept, TInputSchema> & string; } -function getFormServerHandler<TOutput, TInputSchema extends ActionInputSchema<'form'>>( +function getFormServerHandler<TOutput, TInputSchema extends z.ZodType>( handler: ActionHandler<TInputSchema, TOutput>, inputSchema?: TInputSchema, ) { @@ -95,9 +92,14 @@ function getFormServerHandler<TOutput, TInputSchema extends ActionInputSchema<'f }); } - if (!(inputSchema instanceof z.ZodObject)) return await handler(unparsedInput, context); + if (!inputSchema) return await handler(unparsedInput, context); - const parsed = await inputSchema.safeParseAsync(formDataToObject(unparsedInput, inputSchema)); + const baseSchema = unwrapSchemaEffects(inputSchema); + const parsed = await inputSchema.safeParseAsync( + baseSchema instanceof z.ZodObject + ? formDataToObject(unparsedInput, baseSchema) + : unparsedInput, + ); if (!parsed.success) { throw new ActionInputError(parsed.error.issues); } @@ -105,7 +107,7 @@ function getFormServerHandler<TOutput, TInputSchema extends ActionInputSchema<'f }; } -function getJsonServerHandler<TOutput, TInputSchema extends ActionInputSchema<'json'>>( +function getJsonServerHandler<TOutput, TInputSchema extends z.ZodType>( handler: ActionHandler<TInputSchema, TOutput>, inputSchema?: TInputSchema, ) { @@ -131,7 +133,8 @@ export function formDataToObject<T extends z.AnyZodObject>( formData: FormData, schema: T, ): Record<string, unknown> { - const obj: Record<string, unknown> = {}; + const obj: Record<string, unknown> = + schema._def.unknownKeys === 'passthrough' ? Object.fromEntries(formData.entries()) : {}; for (const [key, baseValidator] of Object.entries(schema.shape)) { let validator = baseValidator; @@ -189,3 +192,15 @@ function handleFormDataGet( } return validator instanceof z.ZodNumber ? Number(value) : value; } + +function unwrapSchemaEffects(schema: z.ZodType) { + while (schema instanceof z.ZodEffects || schema instanceof z.ZodPipeline) { + if (schema instanceof z.ZodEffects) { + schema = schema._def.schema; + } + if (schema instanceof z.ZodPipeline) { + schema = schema._def.in; + } + } + return schema; +} diff --git a/packages/astro/src/actions/runtime/virtual/shared.ts b/packages/astro/src/actions/runtime/virtual/shared.ts index d792a9af5..01f9bd4e6 100644 --- a/packages/astro/src/actions/runtime/virtual/shared.ts +++ b/packages/astro/src/actions/runtime/virtual/shared.ts @@ -1,8 +1,16 @@ import { parse as devalueParse, stringify as devalueStringify } from 'devalue'; import type { z } from 'zod'; +import { REDIRECT_STATUS_CODES } from '../../../core/constants.js'; +import { ActionsReturnedInvalidDataError } from '../../../core/errors/errors-data.js'; +import { AstroError } from '../../../core/errors/errors.js'; import { ACTION_QUERY_PARAMS as _ACTION_QUERY_PARAMS } from '../../consts.js'; -import type { ErrorInferenceObject, MaybePromise } from '../utils.js'; +import type { + ErrorInferenceObject, + MaybePromise, + ActionAPIContext as _ActionAPIContext, +} from '../utils.js'; +export type ActionAPIContext = _ActionAPIContext; export const ACTION_QUERY_PARAMS = _ACTION_QUERY_PARAMS; export const ACTION_ERROR_CODES = [ @@ -232,14 +240,30 @@ export function serializeActionResult(res: SafeResult<any, any>): SerializedActi status: 204, }; } + let body; + try { + body = devalueStringify(res.data, { + // Add support for URL objects + URL: (value) => value instanceof URL && value.href, + }); + } catch (e) { + let hint = ActionsReturnedInvalidDataError.hint; + if (res.data instanceof Response) { + hint = REDIRECT_STATUS_CODES.includes(res.data.status as any) + ? 'If you need to redirect when the action succeeds, trigger a redirect where the action is called. See the Actions guide for server and client redirect examples: https://docs.astro.build/en/guides/actions.' + : 'If you need to return a Response object, try using a server endpoint instead. See https://docs.astro.build/en/guides/endpoints/#server-endpoints-api-routes'; + } + throw new AstroError({ + ...ActionsReturnedInvalidDataError, + message: ActionsReturnedInvalidDataError.message(String(e)), + hint, + }); + } return { type: 'data', status: 200, contentType: 'application/json+devalue', - body: devalueStringify(res.data, { - // Add support for URL objects - URL: (value) => value instanceof URL && value.href, - }), + body, }; } diff --git a/packages/astro/src/cli/add/babel.ts b/packages/astro/src/cli/add/babel.ts deleted file mode 100644 index facaabd54..000000000 --- a/packages/astro/src/cli/add/babel.ts +++ /dev/null @@ -1,16 +0,0 @@ -import generator from '@babel/generator'; -import parser from '@babel/parser'; -import traverse from '@babel/traverse'; -import * as t from '@babel/types'; - -export const visit = traverse.default; -export { t }; - -export async function generate(ast: t.File) { - const astToText = generator.default; - const { code } = astToText(ast); - return code; -} - -export const parse = (code: string) => - parser.parse(code, { sourceType: 'unambiguous', plugins: ['typescript'] }); diff --git a/packages/astro/src/cli/add/imports.ts b/packages/astro/src/cli/add/imports.ts deleted file mode 100644 index 375ca1dd8..000000000 --- a/packages/astro/src/cli/add/imports.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { t, visit } from './babel.js'; - -export function ensureImport(root: t.File, importDeclaration: t.ImportDeclaration) { - let specifiersToFind = [...importDeclaration.specifiers]; - - visit(root, { - ImportDeclaration(path) { - if (path.node.source.value === importDeclaration.source.value) { - path.node.specifiers.forEach((specifier) => - specifiersToFind.forEach((specifierToFind, i) => { - if (specifier.type !== specifierToFind.type) return; - if (specifier.local.name === specifierToFind.local.name) { - specifiersToFind.splice(i, 1); - } - }), - ); - } - }, - }); - - if (specifiersToFind.length === 0) return; - - visit(root, { - Program(path) { - const declaration = t.importDeclaration(specifiersToFind, importDeclaration.source); - const latestImport = path - .get('body') - .filter((statement) => statement.isImportDeclaration()) - .pop(); - - if (latestImport) latestImport.insertAfter(declaration); - else path.unshiftContainer('body', declaration); - }, - }); -} diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts index f710184d2..2b230048c 100644 --- a/packages/astro/src/cli/add/index.ts +++ b/packages/astro/src/cli/add/index.ts @@ -3,12 +3,14 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import boxen from 'boxen'; import { diffWords } from 'diff'; -import { execa } from 'execa'; import { bold, cyan, dim, green, magenta, red, yellow } from 'kleur/colors'; +import { type ASTNode, type ProxifiedModule, builders, generateCode, loadFile } from 'magicast'; +import { getDefaultExportOptions } from 'magicast/helpers'; import ora from 'ora'; import preferredPM from 'preferred-pm'; import prompts from 'prompts'; import maxSatisfying from 'semver/ranges/max-satisfying.js'; +import { exec } from 'tinyexec'; import { loadTSConfig, resolveConfig, @@ -30,9 +32,6 @@ import { ensureProcessNodeEnv, parseNpmName } from '../../core/util.js'; import { eventCliSession, telemetry } from '../../events/index.js'; import { type Flags, createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js'; import { fetchPackageJson, fetchPackageVersions } from '../install-package.js'; -import { generate, parse, t, visit } from './babel.js'; -import { ensureImport } from './imports.js'; -import { wrapDefaultExport } from './wrapper.js'; interface AddOptions { flags: Flags; @@ -261,29 +260,26 @@ export async function add(names: string[], { flags }: AddOptions) { await fs.writeFile(fileURLToPath(configURL), STUBS.ASTRO_CONFIG, { encoding: 'utf-8' }); } - let ast: t.File | null = null; + let mod: ProxifiedModule<any> | undefined; try { - ast = await parseAstroConfig(configURL); - + mod = await loadFile(fileURLToPath(configURL)); logger.debug('add', 'Parsed astro config'); - const defineConfig = t.identifier('defineConfig'); - ensureImport( - ast, - t.importDeclaration( - [t.importSpecifier(defineConfig, defineConfig)], - t.stringLiteral('astro/config'), - ), - ); - wrapDefaultExport(ast, defineConfig); - + if (mod.exports.default.$type !== 'function-call') { + // ensure config is wrapped with `defineConfig` + mod.imports.$prepend({ imported: 'defineConfig', from: 'astro/config' }); + mod.exports.default = builders.functionCall('defineConfig', mod.exports.default); + } else if (mod.exports.default.$args[0] == null) { + // ensure first argument of `defineConfig` is not empty + mod.exports.default.$args[0] = {}; + } logger.debug('add', 'Astro config ensured `defineConfig`'); for (const integration of integrations) { if (isAdapter(integration)) { const officialExportName = OFFICIAL_ADAPTER_TO_IMPORT_MAP[integration.id]; if (officialExportName) { - await setAdapter(ast, integration, officialExportName); + setAdapter(mod, integration); } else { logger.info( 'SKIP_FORMAT', @@ -295,7 +291,7 @@ export async function add(names: string[], { flags }: AddOptions) { ); } } else { - await addIntegration(ast, integration); + addIntegration(mod, integration); } logger.debug('add', `Astro config added integration ${integration.id}`); } @@ -306,11 +302,11 @@ export async function add(names: string[], { flags }: AddOptions) { let configResult: UpdateResult | undefined; - if (ast) { + if (mod) { try { configResult = await updateAstroConfig({ configURL, - ast, + mod, flags, logger, logAdapterInstructions: integrations.some(isAdapter), @@ -390,17 +386,6 @@ function isAdapter( return integration.type === 'adapter'; } -async function parseAstroConfig(configURL: URL): Promise<t.File> { - const source = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' }); - const result = parse(source); - - if (!result) throw new Error('Unknown error parsing astro config'); - if (result.errors.length > 0) - throw new Error('Error parsing astro config: ' + JSON.stringify(result.errors)); - - return result; -} - // Convert an arbitrary NPM package name into a JS identifier // Some examples: // - @astrojs/image => image @@ -437,130 +422,47 @@ Documentation: https://docs.astro.build/en/guides/integrations-guide/`; return err; } -async function addIntegration(ast: t.File, integration: IntegrationInfo) { - const integrationId = t.identifier(toIdent(integration.id)); - - ensureImport( - ast, - t.importDeclaration( - [t.importDefaultSpecifier(integrationId)], - t.stringLiteral(integration.packageName), - ), - ); +function addIntegration(mod: ProxifiedModule<any>, integration: IntegrationInfo) { + const config = getDefaultExportOptions(mod); + const integrationId = toIdent(integration.id); - visit(ast, { - // eslint-disable-next-line @typescript-eslint/no-shadow - ExportDefaultDeclaration(path) { - if (!t.isCallExpression(path.node.declaration)) return; - - const configObject = path.node.declaration.arguments[0]; - if (!t.isObjectExpression(configObject)) return; - - let integrationsProp = configObject.properties.find((prop) => { - if (prop.type !== 'ObjectProperty') return false; - if (prop.key.type === 'Identifier') { - if (prop.key.name === 'integrations') return true; - } - if (prop.key.type === 'StringLiteral') { - if (prop.key.value === 'integrations') return true; - } - return false; - }) as t.ObjectProperty | undefined; - - const integrationCall = t.callExpression(integrationId, []); - - if (!integrationsProp) { - configObject.properties.push( - t.objectProperty(t.identifier('integrations'), t.arrayExpression([integrationCall])), - ); - return; - } - - if (integrationsProp.value.type !== 'ArrayExpression') - throw new Error('Unable to parse integrations'); - - const existingIntegrationCall = integrationsProp.value.elements.find( - (expr) => - t.isCallExpression(expr) && - t.isIdentifier(expr.callee) && - expr.callee.name === integrationId.name, - ); - - if (existingIntegrationCall) return; + if (!mod.imports.$items.some((imp) => imp.local === integrationId)) { + mod.imports.$append({ imported: integrationId, from: integration.packageName }); + } - integrationsProp.value.elements.push(integrationCall); - }, - }); + config.integrations ??= []; + if ( + !config.integrations.$ast.elements.some( + (el: ASTNode) => + el.type === 'CallExpression' && + el.callee.type === 'Identifier' && + el.callee.name === integrationId, + ) + ) { + config.integrations.push(builders.functionCall(integrationId)); + } } -async function setAdapter(ast: t.File, adapter: IntegrationInfo, exportName: string) { - const adapterId = t.identifier(toIdent(adapter.id)); +export function setAdapter(mod: ProxifiedModule<any>, adapter: IntegrationInfo) { + const config = getDefaultExportOptions(mod); + const adapterId = toIdent(adapter.id); - ensureImport( - ast, - t.importDeclaration([t.importDefaultSpecifier(adapterId)], t.stringLiteral(exportName)), - ); - - visit(ast, { - // eslint-disable-next-line @typescript-eslint/no-shadow - ExportDefaultDeclaration(path) { - if (!t.isCallExpression(path.node.declaration)) return; - - const configObject = path.node.declaration.arguments[0]; - if (!t.isObjectExpression(configObject)) return; - - let outputProp = configObject.properties.find((prop) => { - if (prop.type !== 'ObjectProperty') return false; - if (prop.key.type === 'Identifier') { - if (prop.key.name === 'output') return true; - } - if (prop.key.type === 'StringLiteral') { - if (prop.key.value === 'output') return true; - } - return false; - }) as t.ObjectProperty | undefined; - - if (!outputProp) { - configObject.properties.push( - t.objectProperty(t.identifier('output'), t.stringLiteral('server')), - ); - } - - let adapterProp = configObject.properties.find((prop) => { - if (prop.type !== 'ObjectProperty') return false; - if (prop.key.type === 'Identifier') { - if (prop.key.name === 'adapter') return true; - } - if (prop.key.type === 'StringLiteral') { - if (prop.key.value === 'adapter') return true; - } - return false; - }) as t.ObjectProperty | undefined; - - let adapterCall; - switch (adapter.id) { - // the node adapter requires a mode - case 'node': { - adapterCall = t.callExpression(adapterId, [ - t.objectExpression([ - t.objectProperty(t.identifier('mode'), t.stringLiteral('standalone')), - ]), - ]); - break; - } - default: { - adapterCall = t.callExpression(adapterId, []); - } - } + if (!mod.imports.$items.some((imp) => imp.local === adapterId)) { + mod.imports.$append({ imported: adapterId, from: adapter.packageName }); + } - if (!adapterProp) { - configObject.properties.push(t.objectProperty(t.identifier('adapter'), adapterCall)); - return; - } + if (!config.output) { + config.output = 'server'; + } - adapterProp.value = adapterCall; - }, - }); + switch (adapter.id) { + case 'node': + config.adapter = builders.functionCall(adapterId, { mode: 'standalone' }); + break; + default: + config.adapter = builders.functionCall(adapterId); + break; + } } const enum UpdateResult { @@ -572,23 +474,25 @@ const enum UpdateResult { async function updateAstroConfig({ configURL, - ast, + mod, flags, logger, logAdapterInstructions, }: { configURL: URL; - ast: t.File; + mod: ProxifiedModule<any>; flags: Flags; logger: Logger; logAdapterInstructions: boolean; }): Promise<UpdateResult> { const input = await fs.readFile(fileURLToPath(configURL), { encoding: 'utf-8' }); - let output = await generate(ast); - const comment = '// https://astro.build/config'; - const defaultExport = 'export default defineConfig'; - output = output.replace(`\n${comment}`, ''); - output = output.replace(`${defaultExport}`, `\n${comment}\n${defaultExport}`); + const output = generateCode(mod, { + format: { + objectCurlySpacing: true, + useTabs: false, + tabWidth: 2, + }, + }).code; if (input === output) { return UpdateResult.none; @@ -755,7 +659,7 @@ async function tryToInstallIntegrations({ if (await askToContinue({ flags })) { const spinner = ora('Installing dependencies...').start(); try { - await execa( + await exec( installCommand.pm, [ installCommand.command, @@ -764,9 +668,11 @@ async function tryToInstallIntegrations({ ...installCommand.dependencies, ], { - cwd, - // reset NODE_ENV to ensure install command run in dev mode - env: { NODE_ENV: undefined }, + nodeOptions: { + cwd, + // reset NODE_ENV to ensure install command run in dev mode + env: { NODE_ENV: undefined }, + }, }, ); spinner.succeed(); diff --git a/packages/astro/src/cli/add/wrapper.ts b/packages/astro/src/cli/add/wrapper.ts deleted file mode 100644 index c86e87698..000000000 --- a/packages/astro/src/cli/add/wrapper.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { t, visit } from './babel.js'; - -export function wrapDefaultExport(ast: t.File, functionIdentifier: t.Identifier) { - visit(ast, { - ExportDefaultDeclaration(path) { - if (!t.isExpression(path.node.declaration)) return; - if ( - t.isCallExpression(path.node.declaration) && - t.isIdentifier(path.node.declaration.callee) && - path.node.declaration.callee.name === functionIdentifier.name - ) - return; - path.node.declaration = t.callExpression(functionIdentifier, [path.node.declaration]); - }, - }); -} diff --git a/packages/astro/src/cli/docs/open.ts b/packages/astro/src/cli/docs/open.ts index 3913ccec4..6f2fe4c82 100644 --- a/packages/astro/src/cli/docs/open.ts +++ b/packages/astro/src/cli/docs/open.ts @@ -1,5 +1,4 @@ -import type { ExecaChildProcess } from 'execa'; -import { execa } from 'execa'; +import { type Result, exec } from 'tinyexec'; /** * Credit: Azhar22 @@ -26,7 +25,7 @@ const getPlatformSpecificCommand = (): [string] | [string, string[]] => { } }; -export async function openInBrowser(url: string): Promise<ExecaChildProcess> { +export async function openInBrowser(url: string): Promise<Result> { const [command, args = []] = getPlatformSpecificCommand(); - return execa(command, [...args, encodeURI(url)]); + return exec(command, [...args, encodeURI(url)]); } diff --git a/packages/astro/src/cli/install-package.ts b/packages/astro/src/cli/install-package.ts index 637390ef3..d61a752de 100644 --- a/packages/astro/src/cli/install-package.ts +++ b/packages/astro/src/cli/install-package.ts @@ -1,11 +1,11 @@ import { createRequire } from 'node:module'; import boxen from 'boxen'; import ci from 'ci-info'; -import { execa } from 'execa'; import { bold, cyan, dim, magenta } from 'kleur/colors'; import ora from 'ora'; import preferredPM from 'preferred-pm'; import prompts from 'prompts'; +import { exec } from 'tinyexec'; import whichPm from 'which-pm'; import type { Logger } from '../core/logger/core.js'; @@ -141,10 +141,10 @@ async function installPackage( if (Boolean(response)) { const spinner = ora('Installing dependencies...').start(); try { - await execa( + await exec( installCommand.pm, [installCommand.command, ...installCommand.flags, ...installCommand.dependencies], - { cwd: cwd }, + { nodeOptions: { cwd: cwd } }, ); spinner.succeed(); @@ -203,8 +203,8 @@ async function getRegistry(): Promise<string> { const fallback = 'https://registry.npmjs.org'; const packageManager = (await preferredPM(process.cwd()))?.name || 'npm'; try { - const { stdout } = await execa(packageManager, ['config', 'get', 'registry']); - _registry = stdout?.trim()?.replace(/\/$/, '') || fallback; + const { stdout } = await exec(packageManager, ['config', 'get', 'registry']); + _registry = stdout.trim()?.replace(/\/$/, '') || fallback; // Detect cases where the shell command returned a non-URL (e.g. a warning) if (!new URL(_registry).host) _registry = fallback; } catch { diff --git a/packages/astro/src/container/pipeline.ts b/packages/astro/src/container/pipeline.ts index 73caa4ecd..167285158 100644 --- a/packages/astro/src/container/pipeline.ts +++ b/packages/astro/src/container/pipeline.ts @@ -1,4 +1,4 @@ -import { type HeadElements, Pipeline } from '../core/base-pipeline.js'; +import { type HeadElements, Pipeline, type TryRewriteResult } from '../core/base-pipeline.js'; import type { SinglePageBuiltModule } from '../core/build/types.js'; import { createModuleScriptElement, @@ -64,11 +64,8 @@ export class ContainerPipeline extends Pipeline { return { links, styles, scripts }; } - async tryRewrite( - payload: RewritePayload, - request: Request, - ): Promise<[RouteData, ComponentInstance, URL]> { - const [foundRoute, finalUrl] = findRouteToRewrite({ + async tryRewrite(payload: RewritePayload, request: Request): Promise<TryRewriteResult> { + const { newUrl, pathname, routeData } = findRouteToRewrite({ payload, request, routes: this.manifest?.routes.map((r) => r.routeData), @@ -77,8 +74,8 @@ export class ContainerPipeline extends Pipeline { base: this.manifest.base, }); - const componentInstance = await this.getComponentByRoute(foundRoute); - return [foundRoute, componentInstance, finalUrl]; + const componentInstance = await this.getComponentByRoute(routeData); + return { componentInstance, routeData, newUrl, pathname }; } insertRoute(route: RouteData, componentInstance: ComponentInstance): void { diff --git a/packages/astro/src/content/loaders/file.ts b/packages/astro/src/content/loaders/file.ts index cbc684a99..75e5e214d 100644 --- a/packages/astro/src/content/loaders/file.ts +++ b/packages/astro/src/content/loaders/file.ts @@ -26,6 +26,8 @@ export function file(fileName: string): Loader { return; } + const normalizedFilePath = posixRelative(fileURLToPath(settings.config.root), filePath); + if (Array.isArray(json)) { if (json.length === 0) { logger.warn(`No items found in ${fileName}`); @@ -39,11 +41,7 @@ export function file(fileName: string): Loader { continue; } const data = await parseData({ id, data: rawItem, filePath }); - store.set({ - id, - data, - filePath: posixRelative(fileURLToPath(settings.config.root), filePath), - }); + store.set({ id, data, filePath: normalizedFilePath }); } } else if (typeof json === 'object') { const entries = Object.entries<Record<string, unknown>>(json); @@ -51,7 +49,7 @@ export function file(fileName: string): Loader { store.clear(); for (const [id, rawItem] of entries) { const data = await parseData({ id, data: rawItem, filePath }); - store.set({ id, data }); + store.set({ id, data, filePath: normalizedFilePath }); } } else { logger.error(`Invalid data in ${fileName}. Must be an array or object.`); diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 27b0e1915..ba76d5e11 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -4,12 +4,13 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import { slug as githubSlug } from 'github-slugger'; import matter from 'gray-matter'; import type { PluginContext } from 'rollup'; -import { type ViteDevServer, normalizePath } from 'vite'; +import type { ViteDevServer } from 'vite'; import xxhash from 'xxhash-wasm'; import { z } from 'zod'; import { AstroError, AstroErrorData, MarkdownError, errorMap } from '../core/errors/index.js'; import { isYAMLException } from '../core/errors/utils.js'; import type { Logger } from '../core/logger/core.js'; +import { normalizePath } from '../core/viteUtils.js'; import type { AstroSettings } from '../types/astro.js'; import type { AstroConfig } from '../types/public/config.js'; import type { ContentEntryType, DataEntryType } from '../types/public/content.js'; diff --git a/packages/astro/src/core/app/pipeline.ts b/packages/astro/src/core/app/pipeline.ts index d42472f50..43ff91fdd 100644 --- a/packages/astro/src/core/app/pipeline.ts +++ b/packages/astro/src/core/app/pipeline.ts @@ -1,7 +1,7 @@ import type { ComponentInstance, ManifestData } from '../../types/astro.js'; import type { RewritePayload } from '../../types/public/common.js'; import type { RouteData, SSRElement, SSRResult } from '../../types/public/internal.js'; -import { Pipeline } from '../base-pipeline.js'; +import { Pipeline, type TryRewriteResult } from '../base-pipeline.js'; import type { SinglePageBuiltModule } from '../build/types.js'; import { RedirectSinglePageBuiltModule } from '../redirects/component.js'; import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js'; @@ -89,8 +89,8 @@ export class AppPipeline extends Pipeline { payload: RewritePayload, request: Request, _sourceRoute: RouteData, - ): Promise<[RouteData, ComponentInstance, URL]> { - const [foundRoute, finalUrl] = findRouteToRewrite({ + ): Promise<TryRewriteResult> { + const { newUrl, pathname, routeData } = findRouteToRewrite({ payload, request, routes: this.manifest?.routes.map((r) => r.routeData), @@ -99,8 +99,8 @@ export class AppPipeline extends Pipeline { base: this.manifest.base, }); - const componentInstance = await this.getComponentByRoute(foundRoute); - return [foundRoute, componentInstance, finalUrl]; + const componentInstance = await this.getComponentByRoute(routeData); + return { newUrl, pathname, componentInstance, routeData }; } async getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> { diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 66140e778..4bf2a350e 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -74,6 +74,7 @@ export type SSRManifest = { export type SSRManifestI18n = { fallback: Record<string, string> | undefined; + fallbackType: 'redirect' | 'rewrite'; strategy: RoutingStrategies; locales: Locales; defaultLocale: string; diff --git a/packages/astro/src/core/base-pipeline.ts b/packages/astro/src/core/base-pipeline.ts index 9d772de9d..2c8199446 100644 --- a/packages/astro/src/core/base-pipeline.ts +++ b/packages/astro/src/core/base-pipeline.ts @@ -94,7 +94,7 @@ export abstract class Pipeline { rewritePayload: RewritePayload, request: Request, sourceRoute: RouteData, - ): Promise<[RouteData, ComponentInstance, URL]>; + ): Promise<TryRewriteResult>; /** * Tells the pipeline how to retrieve a component give a `RouteData` @@ -105,3 +105,10 @@ export abstract class Pipeline { // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface HeadElements extends Pick<SSRResult, 'scripts' | 'styles' | 'links'> {} + +export interface TryRewriteResult { + routeData: RouteData; + componentInstance: ComponentInstance; + newUrl: URL; + pathname: string; +} diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index b2fed3763..0a33d554c 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -14,7 +14,7 @@ import { removeLeadingForwardSlash, removeTrailingForwardSlash, } from '../../core/path.js'; -import { toRoutingStrategy } from '../../i18n/utils.js'; +import { toFallbackType, toRoutingStrategy } from '../../i18n/utils.js'; import { runHookBuildGenerated } from '../../integrations/hooks.js'; import { getOutputDirectory } from '../../prerender/utils.js'; import type { AstroSettings, ComponentInstance } from '../../types/astro.js'; @@ -504,6 +504,7 @@ function createBuildManifest( if (settings.config.i18n) { i18nManifest = { fallback: settings.config.i18n.fallback, + fallbackType: toFallbackType(settings.config.i18n.routing), strategy: toRoutingStrategy(settings.config.i18n.routing, settings.config.i18n.domains), defaultLocale: settings.config.i18n.defaultLocale, locales: settings.config.i18n.locales, diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index 02be5df9f..55cc0d456 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -9,6 +9,7 @@ import type { } from '../../types/public/internal.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; import type { SSRManifest } from '../app/types.js'; +import type { TryRewriteResult } from '../base-pipeline.js'; import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.js'; import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; import { Pipeline } from '../render/index.js'; @@ -265,8 +266,8 @@ export class BuildPipeline extends Pipeline { payload: RewritePayload, request: Request, _sourceRoute: RouteData, - ): Promise<[RouteData, ComponentInstance, URL]> { - const [foundRoute, finalUrl] = findRouteToRewrite({ + ): Promise<TryRewriteResult> { + const { routeData, pathname, newUrl } = findRouteToRewrite({ payload, request, routes: this.options.manifest.routes, @@ -275,8 +276,8 @@ export class BuildPipeline extends Pipeline { base: this.config.base, }); - const componentInstance = await this.getComponentByRoute(foundRoute); - return [foundRoute, componentInstance, finalUrl]; + const componentInstance = await this.getComponentByRoute(routeData); + return { routeData, componentInstance, newUrl, pathname }; } async retrieveSsrEntry(route: RouteData, filePath: string): Promise<SinglePageBuiltModule> { diff --git a/packages/astro/src/core/build/plugins/plugin-content.ts b/packages/astro/src/core/build/plugins/plugin-content.ts index 5fe0b6792..3482657fb 100644 --- a/packages/astro/src/core/build/plugins/plugin-content.ts +++ b/packages/astro/src/core/build/plugins/plugin-content.ts @@ -22,7 +22,7 @@ import { import { isContentCollectionsCacheEnabled } from '../../util.js'; import { addRollupInput } from '../add-rollup-input.js'; import { CHUNKS_PATH, CONTENT_PATH } from '../consts.js'; -import { type BuildInternals } from '../internal.js'; +import type { BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; import { copyFiles } from '../static-build.js'; import type { StaticBuildOptions } from '../types.js'; diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index ef8eb0915..6259d1526 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -4,7 +4,7 @@ import type { OutputChunk } from 'rollup'; import type { Plugin as VitePlugin } from 'vite'; import { getAssetsPrefix } from '../../../assets/utils/getAssetsPrefix.js'; import { normalizeTheLocale } from '../../../i18n/index.js'; -import { toRoutingStrategy } from '../../../i18n/utils.js'; +import { toFallbackType, toRoutingStrategy } from '../../../i18n/utils.js'; import { runHookBuildSsr } from '../../../integrations/hooks.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; import type { @@ -244,6 +244,7 @@ function buildManifest( if (settings.config.i18n) { i18nManifest = { fallback: settings.config.i18n.fallback, + fallbackType: toFallbackType(settings.config.i18n.routing), strategy: toRoutingStrategy(settings.config.i18n.routing, settings.config.i18n.domains), locales: settings.config.i18n.locales, defaultLocale: settings.config.i18n.defaultLocale, diff --git a/packages/astro/src/core/build/plugins/plugin-pages.ts b/packages/astro/src/core/build/plugins/plugin-pages.ts index 2dc400a6c..6996e3342 100644 --- a/packages/astro/src/core/build/plugins/plugin-pages.ts +++ b/packages/astro/src/core/build/plugins/plugin-pages.ts @@ -1,7 +1,7 @@ import type { Plugin as VitePlugin } from 'vite'; import { routeIsRedirect } from '../../redirects/index.js'; import { addRollupInput } from '../add-rollup-input.js'; -import { type BuildInternals } from '../internal.js'; +import type { BuildInternals } from '../internal.js'; import type { AstroBuildPlugin } from '../plugin.js'; import type { StaticBuildOptions } from '../types.js'; import { RENDERERS_MODULE_ID } from './plugin-renderers.js'; diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 5a2f0a611..e52eb560b 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -400,6 +400,7 @@ export const AstroConfigSchema = z.object({ .object({ prefixDefaultLocale: z.boolean().optional().default(false), redirectToDefaultLocale: z.boolean().optional().default(true), + fallbackType: z.enum(['redirect', 'rewrite']).optional().default('redirect'), }) .refine( ({ prefixDefaultLocale, redirectToDefaultLocale }) => { diff --git a/packages/astro/src/core/constants.ts b/packages/astro/src/core/constants.ts index 8e9f5ac74..274f86797 100644 --- a/packages/astro/src/core/constants.ts +++ b/packages/astro/src/core/constants.ts @@ -46,6 +46,11 @@ export const DEFAULT_404_COMPONENT = 'astro-default-404.astro'; export const DEFAULT_500_COMPONENT = 'astro-default-500.astro'; /** + * A response with one of these status codes will create a redirect response. + */ +export const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308, 300, 304] as const; + +/** * A response with one of these status codes will be rewritten * with the result of rendering the respective error page. */ diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 6a5995e3f..471f8f4f1 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -149,7 +149,7 @@ export async function createVite( astroPrefetch({ settings }), astroTransitions({ settings }), astroDevToolbar({ settings, logger }), - vitePluginFileURL({}), + vitePluginFileURL(), astroInternationalization({ settings }), settings.config.experimental.serverIslands && vitePluginServerIslands({ settings }), astroContainer(), diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 475acdbbf..382c025d3 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -541,7 +541,7 @@ export const MissingImageDimension = { message: (missingDimension: 'width' | 'height' | 'both', imageURL: string) => `Missing ${ missingDimension === 'both' ? 'width and height attributes' : `${missingDimension} attribute` - } for ${imageURL}. When using remote images, both dimensions are required unless in order to avoid CLS.`, + } for ${imageURL}. When using remote images, both dimensions are required in order to avoid CLS.`, hint: 'If your image is inside your `src` folder, you probably meant to import it instead. See [the Imports guide for more information](https://docs.astro.build/en/guides/imports/#other-assets). You can also use `inferSize={true}` for remote images to get the original dimensions.', } satisfies ErrorData; /** @@ -1670,6 +1670,7 @@ export const ActionsWithoutServerOutputError = { * - [Actions RFC](https://github.com/withastro/roadmap/blob/actions/proposals/0046-actions.md) * @description * Action was called from a form using a GET request, but only POST requests are supported. This often occurs if `method="POST"` is missing on the form. + * @deprecated Deprecated since version 4.13.2. */ export const ActionsUsedWithForGetError = { name: 'ActionsUsedWithForGetError', @@ -1684,6 +1685,21 @@ export const ActionsUsedWithForGetError = { * @see * - [Actions RFC](https://github.com/withastro/roadmap/blob/actions/proposals/0046-actions.md) * @description + * Action handler returned invalid data. Handlers should return serializable data types, and cannot return a Response object. + */ +export const ActionsReturnedInvalidDataError = { + name: 'ActionsReturnedInvalidDataError', + title: 'Action handler returned invalid data.', + message: (error: string) => + `Action handler returned invalid data. Handlers should return serializable data types like objects, arrays, strings, and numbers. Parse error: ${error}`, + hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue', +} satisfies ErrorData; + +/** + * @docs + * @see + * - [Actions RFC](https://github.com/withastro/roadmap/blob/actions/proposals/0046-actions.md) + * @description * The server received the query string `?_astroAction=name`, but could not find an action with that name. Use the action function's `.queryString` property to retrieve the form `action` URL. */ export const ActionQueryStringInvalidError = { diff --git a/packages/astro/src/core/render-context.ts b/packages/astro/src/core/render-context.ts index 1c0017c30..7c75df4f3 100644 --- a/packages/astro/src/core/render-context.ts +++ b/packages/astro/src/core/render-context.ts @@ -37,7 +37,7 @@ import { type Pipeline, Slots, getParams, getProps } from './render/index.js'; export class RenderContext { // The first route that this instance of the context attempts to render originalRoute: RouteData; - + // The component pattern to send to the users routePattern: string; @@ -111,6 +111,8 @@ export class RenderContext { const { cookies, middleware, pipeline } = this; const { logger, serverLike, streaming, manifest } = pipeline; + const isPrerendered = !serverLike || this.routeData.prerender; + const props = Object.keys(this.props).length > 0 ? this.props @@ -123,7 +125,7 @@ export class RenderContext { serverLike, base: manifest.base, }); - const apiContext = this.createAPIContext(props); + const apiContext = this.createAPIContext(props, isPrerendered); this.counter++; if (this.counter === 4) { @@ -138,13 +140,13 @@ export class RenderContext { if (payload) { pipeline.logger.debug('router', 'Called rewriting to:', payload); // we intentionally let the error bubble up - const [routeData, component] = await pipeline.tryRewrite( + const { routeData, componentInstance: newComponent } = await pipeline.tryRewrite( payload, this.request, this.originalRoute, ); this.routeData = routeData; - componentInstance = component; + componentInstance = newComponent; this.isRewriting = true; this.status = 200; } @@ -210,18 +212,27 @@ export class RenderContext { return response; } - createAPIContext(props: APIContext['props']): APIContext { + createAPIContext(props: APIContext['props'], isPrerendered: boolean): APIContext { const context = this.createActionAPIContext(); + const redirect = (path: string, status = 302) => + new Response(null, { status, headers: { Location: path } }); + return Object.assign(context, { props, + redirect, getActionResult: createGetActionResult(context.locals), callAction: createCallAction(context), + // Used internally by Actions middleware. + // TODO: discuss exposing this information from APIContext. + // middleware runs on prerendered routes in the dev server, + // so this is useful information to have. + _isPrerendered: isPrerendered, }); } async #executeRewrite(reroutePayload: RewritePayload) { this.pipeline.logger.debug('router', 'Calling rewrite: ', reroutePayload); - const [routeData, component, newURL] = await this.pipeline.tryRewrite( + const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite( reroutePayload, this.request, this.originalRoute, @@ -230,25 +241,22 @@ export class RenderContext { if (reroutePayload instanceof Request) { this.request = reroutePayload; } else { - this.request = this.#copyRequest(newURL, this.request); + this.request = this.#copyRequest(newUrl, this.request); } this.url = new URL(this.request.url); this.cookies = new AstroCookies(this.request); - this.params = getParams(routeData, this.url.pathname); - this.pathname = this.url.pathname; + this.params = getParams(routeData, pathname); + this.pathname = pathname; this.isRewriting = true; // we found a route and a component, we can change the status code to 200 this.status = 200; - this.routePattern = getAstroRoutePattern(routeData.component); - return await this.render(component); + return await this.render(componentInstance); } createActionAPIContext(): ActionAPIContext { const renderContext = this; const { cookies, params, pipeline, url } = this; const generator = `Astro v${ASTRO_VERSION}`; - const redirect = (path: string, status = 302) => - new Response(null, { status, headers: { Location: path } }); const rewrite = async (reroutePayload: RewritePayload) => { return await this.#executeRewrite(reroutePayload); @@ -285,7 +293,6 @@ export class RenderContext { get preferredLocaleList() { return renderContext.computePreferredLocaleList(); }, - redirect, rewrite, request: this.request, site: pipeline.site, @@ -580,25 +587,27 @@ export class RenderContext { * @param component */ function getAstroRoutePattern(component: RouteData['component']): string { - let splitComponent = component.split("/"); + let splitComponent = component.split('/'); while (true) { const currentPart = splitComponent.shift(); - if (!currentPart) {break} - + if (!currentPart) { + break; + } + // "pages" isn't configurable, so it's safe to stop here - if (currentPart === "pages") { - break + if (currentPart === 'pages') { + break; } } - - const pathWithoutPages = splitComponent.join("/"); + + const pathWithoutPages = splitComponent.join('/'); // This covers cases where routes don't have extensions, so they can be: [slug] or [...slug] - if (pathWithoutPages.endsWith("]")) { + if (pathWithoutPages.endsWith(']')) { return pathWithoutPages; } - splitComponent = splitComponent.join("/").split("."); + splitComponent = splitComponent.join('/').split('.'); // this should remove the extension splitComponent.pop(); - return "/" + splitComponent.join("/"); + return '/' + splitComponent.join('/'); } diff --git a/packages/astro/src/core/routing/rewrite.ts b/packages/astro/src/core/routing/rewrite.ts index a6fce3354..3ad6a9bd2 100644 --- a/packages/astro/src/core/routing/rewrite.ts +++ b/packages/astro/src/core/routing/rewrite.ts @@ -14,6 +14,17 @@ export type FindRouteToRewrite = { base: AstroConfig['base']; }; +export interface FindRouteToRewriteResult { + routeData: RouteData; + newUrl: URL; + pathname: string; +} + +/** + * Shared logic to retrieve the rewritten route. It returns a tuple that represents: + * 1. The new `Request` object. It contains `base` + * 2. + */ export function findRouteToRewrite({ payload, routes, @@ -21,23 +32,25 @@ export function findRouteToRewrite({ trailingSlash, buildFormat, base, -}: FindRouteToRewrite): [RouteData, URL] { - let finalUrl: URL | undefined = undefined; +}: FindRouteToRewrite): FindRouteToRewriteResult { + let newUrl: URL | undefined = undefined; if (payload instanceof URL) { - finalUrl = payload; + newUrl = payload; } else if (payload instanceof Request) { - finalUrl = new URL(payload.url); + newUrl = new URL(payload.url); } else { - finalUrl = new URL(payload, new URL(request.url).origin); + newUrl = new URL(payload, new URL(request.url).origin); + } + let pathname = newUrl.pathname; + if (base !== '/' && newUrl.pathname.startsWith(base)) { + pathname = shouldAppendForwardSlash(trailingSlash, buildFormat) + ? appendForwardSlash(newUrl.pathname) + : removeTrailingForwardSlash(newUrl.pathname); + pathname = pathname.slice(base.length); } let foundRoute; for (const route of routes) { - const pathname = shouldAppendForwardSlash(trailingSlash, buildFormat) - ? appendForwardSlash(finalUrl.pathname) - : base !== '/' - ? removeTrailingForwardSlash(finalUrl.pathname) - : finalUrl.pathname; if (route.pattern.test(decodeURI(pathname))) { foundRoute = route; break; @@ -45,13 +58,17 @@ export function findRouteToRewrite({ } if (foundRoute) { - return [foundRoute, finalUrl]; + return { + routeData: foundRoute, + newUrl, + pathname, + }; } else { const custom404 = routes.find((route) => route.route === '/404'); if (custom404) { - return [custom404, finalUrl]; + return { routeData: custom404, newUrl, pathname }; } else { - return [DEFAULT_404_ROUTE, finalUrl]; + return { routeData: DEFAULT_404_ROUTE, newUrl, pathname }; } } } diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index c9769443e..9b2973233 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -58,7 +58,7 @@ export default async function sync( } let settings = await createSettings(astroConfig, inlineConfig.root); settings = await runHookConfigSetup({ - command: 'build', + command: 'sync', settings, logger, }); diff --git a/packages/astro/src/core/viteUtils.ts b/packages/astro/src/core/viteUtils.ts index bfe1eaadc..46c59d25d 100644 --- a/packages/astro/src/core/viteUtils.ts +++ b/packages/astro/src/core/viteUtils.ts @@ -1,10 +1,18 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { normalizePath } from 'vite'; -import { prependForwardSlash } from '../core/path.js'; +import { prependForwardSlash, slash } from '../core/path.js'; import type { ModuleLoader } from './module-loader/index.js'; import { VALID_ID_PREFIX, resolveJsToTs, unwrapId, viteID } from './util.js'; +const isWindows = typeof process !== 'undefined' && process.platform === 'win32'; + +/** + * Re-implementation of Vite's normalizePath that can be used without Vite + */ +export function normalizePath(id: string) { + return path.posix.normalize(isWindows ? slash(id) : id); +} + /** * Resolve the hydration paths so that it can be imported in the client */ diff --git a/packages/astro/src/i18n/index.ts b/packages/astro/src/i18n/index.ts index c7e676f75..af08db408 100644 --- a/packages/astro/src/i18n/index.ts +++ b/packages/astro/src/i18n/index.ts @@ -278,6 +278,7 @@ export type MiddlewarePayload = { defaultLocale: string; domains: Record<string, string> | undefined; fallback: Record<string, string> | undefined; + fallbackType: 'redirect' | 'rewrite'; }; // NOTE: public function exported to the users via `astro:i18n` module @@ -328,7 +329,7 @@ export function notFound({ base, locales }: MiddlewarePayload) { } // NOTE: public function exported to the users via `astro:i18n` module -export type RedirectToFallback = (context: APIContext, response: Response) => Response; +export type RedirectToFallback = (context: APIContext, response: Response) => Promise<Response>; export function redirectToFallback({ fallback, @@ -336,8 +337,9 @@ export function redirectToFallback({ defaultLocale, strategy, base, + fallbackType, }: MiddlewarePayload) { - return function (context: APIContext, response: Response): Response { + return async function (context: APIContext, response: Response): Promise<Response> { if (response.status >= 300 && fallback) { const fallbackKeys = fallback ? Object.keys(fallback) : []; // we split the URL using the `/`, and then check in the returned array we have the locale @@ -371,7 +373,12 @@ export function redirectToFallback({ } else { newPathname = context.url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`); } - return context.redirect(newPathname); + + if (fallbackType === 'rewrite') { + return await context.rewrite(newPathname); + } else { + return context.redirect(newPathname); + } } } return response; diff --git a/packages/astro/src/i18n/utils.ts b/packages/astro/src/i18n/utils.ts index 98a44a19c..5dc58908a 100644 --- a/packages/astro/src/i18n/utils.ts +++ b/packages/astro/src/i18n/utils.ts @@ -215,3 +215,12 @@ export function toRoutingStrategy( return strategy; } + +export function toFallbackType( + routing: NonNullable<AstroConfig['i18n']>['routing'], +): 'redirect' | 'rewrite' { + if (routing === 'manual') { + return 'rewrite'; + } + return routing.fallbackType; +} diff --git a/packages/astro/src/integrations/hooks.ts b/packages/astro/src/integrations/hooks.ts index 1dc681db6..8f3e62b8c 100644 --- a/packages/astro/src/integrations/hooks.ts +++ b/packages/astro/src/integrations/hooks.ts @@ -119,7 +119,7 @@ export async function runHookConfigSetup({ fs = fsMod, }: { settings: AstroSettings; - command: 'dev' | 'build' | 'preview'; + command: 'dev' | 'build' | 'preview' | 'sync'; logger: Logger; isRestart?: boolean; fs?: typeof fsMod; diff --git a/packages/astro/src/prefetch/index.ts b/packages/astro/src/prefetch/index.ts index 177945f37..3eb8cd570 100644 --- a/packages/astro/src/prefetch/index.ts +++ b/packages/astro/src/prefetch/index.ts @@ -215,6 +215,9 @@ export interface PrefetchOptions { * @param opts Additional options for prefetching. */ export function prefetch(url: string, opts?: PrefetchOptions) { + // Remove url hash to avoid prefetching the same URL multiple times + url = url.replace(/#.*/, ''); + const ignoreSlowConnection = opts?.ignoreSlowConnection ?? false; if (!canPrefetchUrl(url, ignoreSlowConnection)) return; prefetchedUrls.add(url); diff --git a/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts b/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts index e24698d43..c089664da 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/toolbar.ts @@ -3,7 +3,7 @@ import type { ResolvedDevToolbarApp as DevToolbarAppDefinition } from '../../../ import { type ToolbarAppEventTarget, serverHelpers } from './helpers.js'; import { settings } from './settings.js'; import { type Icon, getIconElement, isDefinedIcon } from './ui-library/icons.js'; -import { type Placement } from './ui-library/window.js'; +import type { Placement } from './ui-library/window.js'; export type DevToolbarApp = DevToolbarAppDefinition & { builtIn: boolean; diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index e32b1a42f..801e1c983 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -1,14 +1,22 @@ import type { ClientDirective } from '../../types/public/integrations.js'; -const idleDirective: ClientDirective = (load) => { +const idleDirective: ClientDirective = (load, options) => { const cb = async () => { const hydrate = await load(); await hydrate(); }; + + const rawOptions = + typeof options.value === 'object' ? (options.value as IdleRequestOptions) : undefined; + + const idleOptions: IdleRequestOptions = { + timeout: rawOptions?.timeout, + }; + if ('requestIdleCallback' in window) { - (window as any).requestIdleCallback(cb); + (window as any).requestIdleCallback(cb, idleOptions); } else { - setTimeout(cb, 200); + setTimeout(cb, idleOptions.timeout || 200); } }; diff --git a/packages/astro/src/transitions/swap-functions.ts b/packages/astro/src/transitions/swap-functions.ts index 4c8db82ee..e2d8557f5 100644 --- a/packages/astro/src/transitions/swap-functions.ts +++ b/packages/astro/src/transitions/swap-functions.ts @@ -133,6 +133,14 @@ const shouldCopyProps = (el: HTMLElement): boolean => { return persistProps == null || persistProps === 'false'; }; +export const swapFunctions = { + deselectScripts, + swapRootAttributes, + swapHeadElements, + swapBodyElement, + saveFocus, +}; + export const swap = (doc: Document) => { deselectScripts(doc); swapRootAttributes(doc); diff --git a/packages/astro/src/transitions/vite-plugin-transitions.ts b/packages/astro/src/transitions/vite-plugin-transitions.ts index 56f086a06..e8122cac4 100644 --- a/packages/astro/src/transitions/vite-plugin-transitions.ts +++ b/packages/astro/src/transitions/vite-plugin-transitions.ts @@ -42,6 +42,7 @@ export default function astroTransitions({ settings }: { settings: AstroSettings TRANSITION_BEFORE_SWAP, isTransitionBeforeSwapEvent, TransitionBeforeSwapEvent, TRANSITION_AFTER_SWAP, TRANSITION_PAGE_LOAD } from "astro/virtual-modules/transitions-events.js"; + export { swapFunctions } from "astro/virtual-modules/transitions-swap-functions.js"; `; } }, diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index b429104e4..9630313ec 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -1298,6 +1298,43 @@ export interface AstroUserConfig { redirectToDefaultLocale?: boolean; /** + * @docs + * @name i18n.routing.fallbackType + * @kind h4 + * @type {"redirect" | "rewrite"} + * @default `"redirect"` + * @version 4.15.0 + * @description + * + * When [`i18n.fallback`](#i18nfallback) is configured to avoid showing a 404 page for missing page routes, this option controls whether to [redirect](https://docs.astro.build/en/guides/routing/#redirects) to the fallback page, or to [rewrite](https://docs.astro.build/en/guides/routing/#rewrites) the fallback page's content in place. + * + * By default, Astro's i18n routing creates pages that redirect your visitors to a new destination based on your fallback configuration. The browser will refresh and show the destination address in the URL bar. + * + * When `i18n.routing.fallback: "rewrite"` is configured, Astro will create pages that render the contents of the fallback page on the original, requested URL. + * + * With the following configuration, if you have the file `src/pages/en/about.astro` but not `src/pages/fr/about.astro`, the `astro build` command will generate `dist/fr/about.html` with the same content as the `dist/en/index.html` page. + * Your site visitor will see the English version of the page at `https://example.com/fr/about/` and will not be redirected. + * + * ```js + * //astro.config.mjs + * export default defineConfig({ + * i18n: { + * defaultLocale: "en", + * locales: ["en", "fr"], + * routing: { + * prefixDefaultLocale: false, + * fallbackType: "rewrite", + * }, + * fallback: { + * fr: "en", + * } + * }, + * }) + * ``` + */ + fallbackType: 'redirect' | 'rewrite'; + + /** * @name i18n.routing.strategy * @type {"pathname"} * @default `"pathname"` @@ -1775,7 +1812,7 @@ export interface AstroUserConfig { * ``` * * :::note - * Loaders will not automatically [exclude files prefaced with an `_`](/en/guides/routing/#excluding-pages). Use a regular expression such as `pattern: '**\/[^_]*.md` in your loader to ignore these files. + * Loaders will not automatically [exclude files prefaced with an `_`](/en/guides/routing/#excluding-pages). Use a regular expression such as `pattern: '**\/[^_]*.md'` in your loader to ignore these files. * ::: * * #### Querying and rendering with the Content Layer API @@ -1807,7 +1844,7 @@ export interface AstroUserConfig { * * const post = await getEntry('blog', Astro.params.slug); * - * const { Content, headings } = await render(entry); + * const { Content, headings } = await render(post); * --- * * <Content /> diff --git a/packages/astro/src/virtual-modules/i18n.ts b/packages/astro/src/virtual-modules/i18n.ts index 927d479aa..8f85ae5f6 100644 --- a/packages/astro/src/virtual-modules/i18n.ts +++ b/packages/astro/src/virtual-modules/i18n.ts @@ -3,7 +3,7 @@ import { IncorrectStrategyForI18n } from '../core/errors/errors-data.js'; import { AstroError } from '../core/errors/index.js'; import * as I18nInternals from '../i18n/index.js'; import type { RedirectToFallback } from '../i18n/index.js'; -import { toRoutingStrategy } from '../i18n/utils.js'; +import { toFallbackType, toRoutingStrategy } from '../i18n/utils.js'; import type { I18nInternalConfig } from '../i18n/vite-plugin-i18n.js'; import type { MiddlewareHandler } from '../types/public/common.js'; import type { AstroConfig, ValidRedirectStatus } from '../types/public/config.js'; @@ -18,6 +18,7 @@ const { defaultLocale, locales, domains, fallback, routing } = i18n!; const base = import.meta.env.BASE_URL; let strategy = toRoutingStrategy(routing, domains); +let fallbackType = toFallbackType(routing); export type GetLocaleOptions = I18nInternals.GetLocaleOptions; @@ -264,6 +265,7 @@ if (i18n?.routing === 'manual') { strategy, domains, fallback, + fallbackType, }); } else { redirectToDefaultLocale = noop('redirectToDefaultLocale'); @@ -292,6 +294,7 @@ if (i18n?.routing === 'manual') { strategy, domains, fallback, + fallbackType, }); } else { notFound = noop('notFound'); @@ -314,7 +317,7 @@ if (i18n?.routing === 'manual') { * Allows to use the build-in fallback system of Astro * * @param {APIContext} context The context passed to the middleware - * @param {Response} response An optional `Response` in case you're handling a `Response` coming from the `next` function. + * @param {Promise<Response>} response An optional `Response` in case you're handling a `Response` coming from the `next` function. */ export let redirectToFallback: RedirectToFallback; @@ -328,6 +331,7 @@ if (i18n?.routing === 'manual') { strategy, domains, fallback, + fallbackType, }); } else { redirectToFallback = noop('useFallback'); @@ -371,11 +375,13 @@ export let middleware: (customOptions: NewAstroRoutingConfigWithoutManual) => Mi if (i18n?.routing === 'manual') { middleware = (customOptions: NewAstroRoutingConfigWithoutManual) => { strategy = toRoutingStrategy(customOptions, {}); + fallbackType = toFallbackType(customOptions); const manifest: SSRManifest['i18n'] = { ...i18n, fallback: undefined, strategy, domainLookupTable: {}, + fallbackType, }; return I18nInternals.createMiddleware(manifest, base, trailingSlash, format); }; diff --git a/packages/astro/src/virtual-modules/transitions-swap-functions.ts b/packages/astro/src/virtual-modules/transitions-swap-functions.ts new file mode 100644 index 000000000..5947533e3 --- /dev/null +++ b/packages/astro/src/virtual-modules/transitions-swap-functions.ts @@ -0,0 +1 @@ +export * from '../transitions/swap-functions.js'; diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts index 63384f87b..b22a3653d 100644 --- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts +++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url'; import { getInfoOutput } from '../cli/info/index.js'; -import { type HeadElements } from '../core/base-pipeline.js'; +import type { HeadElements, TryRewriteResult } from '../core/base-pipeline.js'; import { ASTRO_VERSION } from '../core/constants.js'; import { enhanceViteSSRError } from '../core/errors/dev/index.js'; import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js'; @@ -197,11 +197,11 @@ export class DevPipeline extends Pipeline { payload: RewritePayload, request: Request, _sourceRoute: RouteData, - ): Promise<[RouteData, ComponentInstance, URL]> { + ): Promise<TryRewriteResult> { if (!this.manifestData) { throw new Error('Missing manifest data. This is an internal error, please file an issue.'); } - const [foundRoute, finalUrl] = findRouteToRewrite({ + const { routeData, pathname, newUrl } = findRouteToRewrite({ payload, request, routes: this.manifestData?.routes, @@ -210,8 +210,8 @@ export class DevPipeline extends Pipeline { base: this.config.base, }); - const componentInstance = await this.getComponentByRoute(foundRoute); - return [foundRoute, componentInstance, finalUrl]; + const componentInstance = await this.getComponentByRoute(routeData); + return { newUrl, pathname, componentInstance, routeData }; } setManifestData(manifestData: ManifestData) { diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 414034561..f1cfa16ba 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -11,7 +11,7 @@ import type { Logger } from '../core/logger/core.js'; import { createViteLoader } from '../core/module-loader/index.js'; import { injectDefaultRoutes } from '../core/routing/default.js'; import { createRouteManifest } from '../core/routing/index.js'; -import { toRoutingStrategy } from '../i18n/utils.js'; +import { toFallbackType, toRoutingStrategy } from '../i18n/utils.js'; import type { AstroSettings, ManifestData } from '../types/astro.js'; import { baseMiddleware } from './base.js'; import { createController } from './controller.js'; @@ -128,6 +128,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest defaultLocale: settings.config.i18n.defaultLocale, locales: settings.config.i18n.locales, domainLookupTable: {}, + fallbackType: toFallbackType(settings.config.i18n.routing), }; } diff --git a/packages/astro/src/vite-plugin-fileurl/index.ts b/packages/astro/src/vite-plugin-fileurl/index.ts index 4a14323a0..73132f3af 100644 --- a/packages/astro/src/vite-plugin-fileurl/index.ts +++ b/packages/astro/src/vite-plugin-fileurl/index.ts @@ -1,8 +1,9 @@ import type { Plugin as VitePlugin } from 'vite'; -export default function vitePluginFileURL({}): VitePlugin { +export default function vitePluginFileURL(): VitePlugin { return { name: 'astro:vite-plugin-file-url', + enforce: 'pre', resolveId(source, importer) { if (source.startsWith('file://')) { const rest = source.slice(7); diff --git a/packages/astro/src/vite-plugin-scanner/index.ts b/packages/astro/src/vite-plugin-scanner/index.ts index 2633743ae..5923377a4 100644 --- a/packages/astro/src/vite-plugin-scanner/index.ts +++ b/packages/astro/src/vite-plugin-scanner/index.ts @@ -2,7 +2,7 @@ import { extname } from 'node:path'; import { bold } from 'kleur/colors'; import type { Plugin as VitePlugin } from 'vite'; import { normalizePath } from 'vite'; -import { type Logger } from '../core/logger/core.js'; +import type { Logger } from '../core/logger/core.js'; import { isEndpoint, isPage, isServerLikeOutput } from '../core/util.js'; import { rootRelativePath } from '../core/viteUtils.js'; import { runHookRouteSetup } from '../integrations/hooks.js'; diff --git a/packages/astro/templates/actions.mjs b/packages/astro/templates/actions.mjs index 823699e15..3ff65cecf 100644 --- a/packages/astro/templates/actions.mjs +++ b/packages/astro/templates/actions.mjs @@ -93,7 +93,9 @@ async function handleAction(param, path, context) { body, headers, }); - if (rawResult.status === 204) return; + if (rawResult.status === 204) { + return deserializeActionResult({ type: 'empty', status: 204 }); + } return deserializeActionResult({ type: rawResult.ok ? 'data' : 'error', diff --git a/packages/astro/test/actions.test.js b/packages/astro/test/actions.test.js index 341e7c8d6..3c803972c 100644 --- a/packages/astro/test/actions.test.js +++ b/packages/astro/test/actions.test.js @@ -2,6 +2,8 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import * as devalue from 'devalue'; +import { serializeActionResult } from '../dist/actions/runtime/virtual/shared.js'; +import { REDIRECT_STATUS_CODES } from '../dist/core/constants.js'; import testAdapter from './test-adapter.js'; import { loadFixture } from './test-utils.js'; @@ -25,6 +27,28 @@ describe('Astro Actions', () => { await devServer.stop(); }); + it('Does not process middleware cookie for prerendered routes', async () => { + const cookie = new URLSearchParams(); + cookie.append( + '_astroActionPayload', + JSON.stringify({ + actionName: 'subscribe', + actionResult: serializeActionResult({ + data: { channel: 'bholmesdev', subscribeButtonState: 'smashed' }, + error: undefined, + }), + }), + ); + const res = await fixture.fetch('/subscribe-prerendered', { + headers: { + Cookie: cookie.toString(), + }, + }); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal($('body').text().trim(), 'No cookie found.'); + }); + it('Exposes subscribe action', async () => { const res = await fixture.fetch('/_actions/subscribe', { method: 'POST', @@ -217,6 +241,71 @@ describe('Astro Actions', () => { assert.ok($('#user')); }); + it('Supports effects on form input validators', async () => { + const formData = new FormData(); + formData.set('password', 'benisawesome'); + formData.set('confirmPassword', 'benisveryawesome'); + + const req = new Request('http://example.com/_actions/validatePassword', { + method: 'POST', + body: formData, + }); + + const res = await app.render(req); + + assert.equal(res.ok, false); + assert.equal(res.status, 400); + assert.equal(res.headers.get('Content-Type'), 'application/json'); + + const data = await res.json(); + assert.equal(data.type, 'AstroActionInputError'); + assert.equal(data.issues?.[0]?.message, 'Passwords do not match'); + }); + + it('Supports complex chained effects on form input validators', async () => { + const formData = new FormData(); + formData.set('currentPassword', 'benisboring'); + formData.set('newPassword', 'benisawesome'); + formData.set('confirmNewPassword', 'benisawesome'); + + const req = new Request('http://example.com/_actions/validatePasswordComplex', { + method: 'POST', + body: formData, + }); + + const res = await app.render(req); + + assert.equal(res.ok, true); + assert.equal(res.headers.get('Content-Type'), 'application/json+devalue'); + + const data = devalue.parse(await res.text()); + assert.equal(Object.keys(data).length, 2, 'More keys than expected'); + assert.deepEqual(data, { + currentPassword: 'benisboring', + newPassword: 'benisawesome', + }); + }); + + it('Supports input form data transforms', async () => { + const formData = new FormData(); + formData.set('name', 'ben'); + formData.set('age', '42'); + + const req = new Request('http://example.com/_actions/transformFormInput', { + method: 'POST', + body: formData, + }); + + const res = await app.render(req); + + assert.equal(res.ok, true); + assert.equal(res.headers.get('Content-Type'), 'application/json+devalue'); + + const data = devalue.parse(await res.text()); + assert.equal(data?.name, 'ben'); + assert.equal(data?.age, '42'); + }); + describe('legacy', () => { it('Response middleware fallback', async () => { const formData = new FormData(); @@ -348,8 +437,6 @@ describe('Astro Actions', () => { }); }); -const validRedirectStatuses = new Set([301, 302, 303, 304, 307, 308]); - /** * Follow an expected redirect response. * @@ -360,7 +447,7 @@ const validRedirectStatuses = new Set([301, 302, 303, 304, 307, 308]); async function followExpectedRedirect(req, app) { const redirect = await app.render(req, { addCookieHeader: true }); assert.ok( - validRedirectStatuses.has(redirect.status), + REDIRECT_STATUS_CODES.includes(redirect.status), `Expected redirect status, got ${redirect.status}`, ); diff --git a/packages/astro/test/fixtures/0-css/package.json b/packages/astro/test/fixtures/0-css/package.json index 394045795..782bddc06 100644 --- a/packages/astro/test/fixtures/0-css/package.json +++ b/packages/astro/test/fixtures/0-css/package.json @@ -9,7 +9,7 @@ "astro": "workspace:*", "react": "^18.3.1", "react-dom": "^18.3.1", - "svelte": "^4.2.18", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/test/fixtures/actions/src/actions/index.ts b/packages/astro/test/fixtures/actions/src/actions/index.ts index bc61ade3a..881656994 100644 --- a/packages/astro/test/fixtures/actions/src/actions/index.ts +++ b/packages/astro/test/fixtures/actions/src/actions/index.ts @@ -1,5 +1,10 @@ import { defineAction, ActionError, z } from 'astro:actions'; +const passwordSchema = z + .string() + .min(8, 'Password should be at least 8 chars length') + .max(128, 'Password length exceeded. Max 128 chars.'); + export const server = { subscribe: defineAction({ input: z.object({ channel: z.string() }), @@ -44,7 +49,56 @@ export const server = { accept: 'form', handler: async (_, { locals }) => { return locals.user; - } + }, + }), + validatePassword: defineAction({ + accept: 'form', + input: z + .object({ password: z.string(), confirmPassword: z.string() }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match', + }), + handler: async ({ password }) => { + return password; + }, + }), + validatePasswordComplex: defineAction({ + accept: 'form', + input: z + .object({ + currentPassword: passwordSchema, + newPassword: passwordSchema, + confirmNewPassword: passwordSchema, + }) + .required() + .refine( + ({ newPassword, confirmNewPassword }) => newPassword === confirmNewPassword, + 'The new password confirmation does not match', + ) + .refine( + ({ currentPassword, newPassword }) => currentPassword !== newPassword, + 'The old password and the new password must not match', + ) + .transform((input) => ({ + currentPassword: input.currentPassword, + newPassword: input.newPassword, + })) + .pipe( + z.object({ + currentPassword: passwordSchema, + newPassword: passwordSchema, + }), + ), + handler: async (data) => { + return data; + }, + }), + transformFormInput: defineAction({ + accept: 'form', + input: z.instanceof(FormData).transform((formData) => Object.fromEntries(formData.entries())), + handler: async (data) => { + return data; + }, }), getUserOrThrow: defineAction({ accept: 'form', @@ -57,22 +111,22 @@ export const server = { }); } return locals.user; - } + }, }), fireAndForget: defineAction({ handler: async () => { return; - } + }, }), zero: defineAction({ handler: async () => { return 0; - } + }, }), false: defineAction({ handler: async () => { return false; - } + }, }), complexValues: defineAction({ handler: async () => { @@ -80,7 +134,7 @@ export const server = { date: new Date(), set: new Set(), url: new URL('https://example.com'), - } - } - }) + }; + }, + }), }; diff --git a/packages/astro/test/fixtures/actions/src/pages/subscribe-prerendered.astro b/packages/astro/test/fixtures/actions/src/pages/subscribe-prerendered.astro new file mode 100644 index 000000000..3d3b37772 --- /dev/null +++ b/packages/astro/test/fixtures/actions/src/pages/subscribe-prerendered.astro @@ -0,0 +1,17 @@ +--- +import { actions } from 'astro:actions'; + +export const prerender = true; + +const result = Astro.getActionResult(actions.subscribe); +--- + +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Document</title> + </head> + <body>{result?.data?.subscribeButtonState ?? 'No cookie found.'}</body> +</html> diff --git a/packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json b/packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json index 6a6cb3491..0ae8cb82c 100644 --- a/packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json +++ b/packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json @@ -5,6 +5,6 @@ "dependencies": { "@astrojs/svelte": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/alias-tsconfig/package.json b/packages/astro/test/fixtures/alias-tsconfig/package.json index 56047eede..833a0a068 100644 --- a/packages/astro/test/fixtures/alias-tsconfig/package.json +++ b/packages/astro/test/fixtures/alias-tsconfig/package.json @@ -6,6 +6,6 @@ "@astrojs/svelte": "workspace:*", "@test/namespace-package": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/alias/package.json b/packages/astro/test/fixtures/alias/package.json index 06d4c32ac..bd0599c5d 100644 --- a/packages/astro/test/fixtures/alias/package.json +++ b/packages/astro/test/fixtures/alias/package.json @@ -5,6 +5,6 @@ "dependencies": { "@astrojs/svelte": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/astro-children/package.json b/packages/astro/test/fixtures/astro-children/package.json index d2e7a6d5e..038487d67 100644 --- a/packages/astro/test/fixtures/astro-children/package.json +++ b/packages/astro/test/fixtures/astro-children/package.json @@ -8,7 +8,7 @@ "@astrojs/vue": "workspace:*", "astro": "workspace:*", "preact": "^10.23.2", - "svelte": "^4.2.18", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/test/fixtures/astro-client-only/package.json b/packages/astro/test/fixtures/astro-client-only/package.json index 02eae8101..e6f71f353 100644 --- a/packages/astro/test/fixtures/astro-client-only/package.json +++ b/packages/astro/test/fixtures/astro-client-only/package.json @@ -9,6 +9,6 @@ "astro": "workspace:*", "react": "^18.3.1", "react-dom": "^18.3.1", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/astro-dynamic/package.json b/packages/astro/test/fixtures/astro-dynamic/package.json index 30c80157b..3d606041a 100644 --- a/packages/astro/test/fixtures/astro-dynamic/package.json +++ b/packages/astro/test/fixtures/astro-dynamic/package.json @@ -8,6 +8,6 @@ "astro": "workspace:*", "react": "^18.3.1", "react-dom": "^18.3.1", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/astro-slots-nested/package.json b/packages/astro/test/fixtures/astro-slots-nested/package.json index 229bd2560..4f3ed29e0 100644 --- a/packages/astro/test/fixtures/astro-slots-nested/package.json +++ b/packages/astro/test/fixtures/astro-slots-nested/package.json @@ -12,8 +12,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/test/fixtures/component-library/package.json b/packages/astro/test/fixtures/component-library/package.json index a8b89e9ee..96f5cecac 100644 --- a/packages/astro/test/fixtures/component-library/package.json +++ b/packages/astro/test/fixtures/component-library/package.json @@ -11,6 +11,6 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/css-dangling-references/package.json b/packages/astro/test/fixtures/css-dangling-references/package.json index 2b9a90cb6..be0392db0 100644 --- a/packages/astro/test/fixtures/css-dangling-references/package.json +++ b/packages/astro/test/fixtures/css-dangling-references/package.json @@ -5,6 +5,6 @@ "dependencies": { "@astrojs/svelte": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } }
\ No newline at end of file diff --git a/packages/astro/test/fixtures/error-bad-js/src/env.d.ts b/packages/astro/test/fixtures/error-bad-js/src/env.d.ts deleted file mode 100644 index f964fe0cf..000000000 --- a/packages/astro/test/fixtures/error-bad-js/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="astro/client" /> diff --git a/packages/astro/test/fixtures/error-build-location/src/env.d.ts b/packages/astro/test/fixtures/error-build-location/src/env.d.ts deleted file mode 100644 index f964fe0cf..000000000 --- a/packages/astro/test/fixtures/error-build-location/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="astro/client" /> diff --git a/packages/astro/test/fixtures/error-non-error/src/env.d.ts b/packages/astro/test/fixtures/error-non-error/src/env.d.ts deleted file mode 100644 index f964fe0cf..000000000 --- a/packages/astro/test/fixtures/error-non-error/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="astro/client" /> diff --git a/packages/astro/test/fixtures/fetch/package.json b/packages/astro/test/fixtures/fetch/package.json index 52f60a20f..97aa25a78 100644 --- a/packages/astro/test/fixtures/fetch/package.json +++ b/packages/astro/test/fixtures/fetch/package.json @@ -8,7 +8,7 @@ "@astrojs/vue": "workspace:*", "astro": "workspace:*", "preact": "^10.23.2", - "svelte": "^4.2.18", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/test/fixtures/jsx/package.json b/packages/astro/test/fixtures/jsx/package.json index 2e45e6575..6d32dffe4 100644 --- a/packages/astro/test/fixtures/jsx/package.json +++ b/packages/astro/test/fixtures/jsx/package.json @@ -15,8 +15,8 @@ "preact": "^10.23.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/test/fixtures/large-array/package.json b/packages/astro/test/fixtures/large-array/package.json index 9f1f25828..0bf422b67 100644 --- a/packages/astro/test/fixtures/large-array/package.json +++ b/packages/astro/test/fixtures/large-array/package.json @@ -5,6 +5,6 @@ "dependencies": { "@astrojs/solid-js": "workspace:*", "astro": "workspace:*", - "solid-js": "^1.8.21" + "solid-js": "^1.8.22" } } diff --git a/packages/astro/test/fixtures/postcss/package.json b/packages/astro/test/fixtures/postcss/package.json index 7ea4f8378..fad256924 100644 --- a/packages/astro/test/fixtures/postcss/package.json +++ b/packages/astro/test/fixtures/postcss/package.json @@ -9,8 +9,8 @@ "astro": "workspace:*", "autoprefixer": "^10.4.20", "postcss": "^8.4.41", - "solid-js": "^1.8.21", - "svelte": "^4.2.18", + "solid-js": "^1.8.22", + "svelte": "^4.2.19", "vue": "^3.4.38" }, "devDependencies": { diff --git a/packages/astro/test/fixtures/preact-component/src/components/SignalsInArray.jsx b/packages/astro/test/fixtures/preact-component/src/components/SignalsInArray.jsx new file mode 100644 index 000000000..69940f730 --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/components/SignalsInArray.jsx @@ -0,0 +1,8 @@ +import { h } from 'preact'; + +export default ({ signalsArray }) => { + return <div class="preact-signal-array"> + <h1>{signalsArray[0]} {signalsArray[3]}</h1> + <p>{signalsArray[1].value}-{signalsArray[2].value}-{signalsArray[4].value}</p> + </div> +} diff --git a/packages/astro/test/fixtures/preact-component/src/components/SignalsInObject.jsx b/packages/astro/test/fixtures/preact-component/src/components/SignalsInObject.jsx new file mode 100644 index 000000000..6187ce8c5 --- /dev/null +++ b/packages/astro/test/fixtures/preact-component/src/components/SignalsInObject.jsx @@ -0,0 +1,8 @@ +import { h } from 'preact'; + +export default ({ signalsObject }) => { + return <div class="preact-signal-object"> + <h1>{signalsObject.title}</h1> + <p>{signalsObject.counter.value}</p> + </div> +} diff --git a/packages/astro/test/fixtures/preact-component/src/pages/signals.astro b/packages/astro/test/fixtures/preact-component/src/pages/signals.astro index b68fde36d..37b43a73c 100644 --- a/packages/astro/test/fixtures/preact-component/src/pages/signals.astro +++ b/packages/astro/test/fixtures/preact-component/src/pages/signals.astro @@ -1,7 +1,10 @@ --- import { signal } from '@preact/signals'; import Signals from '../components/Signals'; +import SignalsInArray from '../components/SignalsInArray'; +import SignalsInObject from '../components/SignalsInObject'; const count = signal(1); +const secondCount = signal(2); --- <html> <head> @@ -10,5 +13,7 @@ const count = signal(1); <body> <Signals client:load count={count} /> <Signals client:load count={count} /> + <SignalsInArray client:load signalsArray={["I'm not a signal", count, count, 12345, secondCount]} /> + <SignalsInObject client:load signalsObject={{title:'I am a title', counter: count}} /> </body> </html> diff --git a/packages/astro/test/fixtures/react-and-solid/package.json b/packages/astro/test/fixtures/react-and-solid/package.json index dbb45a68f..5df316b51 100644 --- a/packages/astro/test/fixtures/react-and-solid/package.json +++ b/packages/astro/test/fixtures/react-and-solid/package.json @@ -7,6 +7,6 @@ "astro": "workspace:*", "react": "^18.3.1", "react-dom": "^18.3.1", - "solid-js": "^1.8.21" + "solid-js": "^1.8.22" } } diff --git a/packages/astro/test/fixtures/rewrite-server/src/pages/[slug]/title.astro b/packages/astro/test/fixtures/rewrite-server/src/pages/[slug]/title.astro index d468d103b..bbc1a2d9e 100644 --- a/packages/astro/test/fixtures/rewrite-server/src/pages/[slug]/title.astro +++ b/packages/astro/test/fixtures/rewrite-server/src/pages/[slug]/title.astro @@ -1,6 +1,5 @@ --- const { slug } = Astro.params; -console.log("is it here???", Astro.params) export const prerender = false; --- <html> diff --git a/packages/astro/test/fixtures/rewrite-trailing-slash-never/src/pages/foo.astro b/packages/astro/test/fixtures/rewrite-trailing-slash-never/src/pages/foo.astro index 70ce07395..f7e38bbc5 100644 --- a/packages/astro/test/fixtures/rewrite-trailing-slash-never/src/pages/foo.astro +++ b/packages/astro/test/fixtures/rewrite-trailing-slash-never/src/pages/foo.astro @@ -1,3 +1,3 @@ --- -return Astro.rewrite("/") +return Astro.rewrite("/base") --- diff --git a/packages/astro/test/fixtures/server-islands/hybrid/package.json b/packages/astro/test/fixtures/server-islands/hybrid/package.json index fdb447b0e..03e184e63 100644 --- a/packages/astro/test/fixtures/server-islands/hybrid/package.json +++ b/packages/astro/test/fixtures/server-islands/hybrid/package.json @@ -5,6 +5,6 @@ "dependencies": { "@astrojs/svelte": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/server-islands/ssr/package.json b/packages/astro/test/fixtures/server-islands/ssr/package.json index fa6e000dd..16e044fe3 100644 --- a/packages/astro/test/fixtures/server-islands/ssr/package.json +++ b/packages/astro/test/fixtures/server-islands/ssr/package.json @@ -5,6 +5,6 @@ "dependencies": { "@astrojs/svelte": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/slots-solid/package.json b/packages/astro/test/fixtures/slots-solid/package.json index 55d2cfa32..59ebea174 100644 --- a/packages/astro/test/fixtures/slots-solid/package.json +++ b/packages/astro/test/fixtures/slots-solid/package.json @@ -6,6 +6,6 @@ "@astrojs/mdx": "workspace:*", "@astrojs/solid-js": "workspace:*", "astro": "workspace:*", - "solid-js": "^1.8.21" + "solid-js": "^1.8.22" } } diff --git a/packages/astro/test/fixtures/slots-svelte/package.json b/packages/astro/test/fixtures/slots-svelte/package.json index 94d15cad2..ddfa80d33 100644 --- a/packages/astro/test/fixtures/slots-svelte/package.json +++ b/packages/astro/test/fixtures/slots-svelte/package.json @@ -6,6 +6,6 @@ "@astrojs/mdx": "workspace:*", "@astrojs/svelte": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/solid-component/deps/solid-jsx-component/package.json b/packages/astro/test/fixtures/solid-component/deps/solid-jsx-component/package.json index 32042224f..976ba6604 100644 --- a/packages/astro/test/fixtures/solid-component/deps/solid-jsx-component/package.json +++ b/packages/astro/test/fixtures/solid-component/deps/solid-jsx-component/package.json @@ -10,6 +10,6 @@ } }, "dependencies": { - "solid-js": "^1.8.21" + "solid-js": "^1.8.22" } } diff --git a/packages/astro/test/fixtures/solid-component/package.json b/packages/astro/test/fixtures/solid-component/package.json index f1f87f2b2..2dc56f6cc 100644 --- a/packages/astro/test/fixtures/solid-component/package.json +++ b/packages/astro/test/fixtures/solid-component/package.json @@ -7,6 +7,6 @@ "@solidjs/router": "^0.14.3", "@test/solid-jsx-component": "file:./deps/solid-jsx-component", "astro": "workspace:*", - "solid-js": "^1.8.21" + "solid-js": "^1.8.22" } } diff --git a/packages/astro/test/fixtures/ssr-prerender-chunks/package.json b/packages/astro/test/fixtures/ssr-prerender-chunks/package.json index 8c8adac13..c386358b8 100644 --- a/packages/astro/test/fixtures/ssr-prerender-chunks/package.json +++ b/packages/astro/test/fixtures/ssr-prerender-chunks/package.json @@ -5,7 +5,7 @@ "dependencies": { "@astrojs/react": "workspace:*", "@test/ssr-prerender-chunks-test-adapter": "link:./deps/test-adapter", - "@types/react": "^18.3.3", + "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "astro": "workspace:*", "react": "^18.3.1", diff --git a/packages/astro/test/fixtures/svelte-component/package.json b/packages/astro/test/fixtures/svelte-component/package.json index 42b4ca310..830d980b7 100644 --- a/packages/astro/test/fixtures/svelte-component/package.json +++ b/packages/astro/test/fixtures/svelte-component/package.json @@ -5,6 +5,6 @@ "dependencies": { "@astrojs/svelte": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18" + "svelte": "^4.2.19" } } diff --git a/packages/astro/test/fixtures/vue-with-multi-renderer/package.json b/packages/astro/test/fixtures/vue-with-multi-renderer/package.json index 65be000bf..f91b6a9c3 100644 --- a/packages/astro/test/fixtures/vue-with-multi-renderer/package.json +++ b/packages/astro/test/fixtures/vue-with-multi-renderer/package.json @@ -6,7 +6,7 @@ "@astrojs/svelte": "workspace:*", "@astrojs/vue": "workspace:*", "astro": "workspace:*", - "svelte": "^4.2.18", + "svelte": "^4.2.19", "vue": "^3.4.38" } } diff --git a/packages/astro/test/i18n-routing.test.js b/packages/astro/test/i18n-routing.test.js index df8083b81..ddb31762f 100644 --- a/packages/astro/test/i18n-routing.test.js +++ b/packages/astro/test/i18n-routing.test.js @@ -1929,3 +1929,106 @@ describe('SSR fallback from missing locale index to default locale index', () => assert.equal(response.headers.get('location'), '/'); }); }); + +describe('Fallback rewrite dev server', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/i18n-routing-fallback/', + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + routing: { + prefixDefaultLocale: false, + }, + fallback: { + fr: 'en', + }, + fallbackType: 'rewrite', + }, + }); + devServer = await fixture.startDevServer(); + }); + after(async () => { + devServer.stop(); + }); + + it('should correctly rewrite to en', async () => { + const html = await fixture.fetch('/fr').then((res) => res.text()); + assert.match(html, /Hello/); + // assert.fail() + }); +}); + +describe('Fallback rewrite SSG', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/i18n-routing-fallback/', + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + routing: { + prefixDefaultLocale: false, + fallbackType: 'rewrite', + }, + fallback: { + fr: 'en', + }, + }, + }); + await fixture.build(); + // app = await fixture.loadTestAdapterApp(); + }); + + it('should correctly rewrite to en', async () => { + const html = await fixture.readFile('/fr/index.html'); + assert.match(html, /Hello/); + // assert.fail() + }); +}); + +describe('Fallback rewrite SSR', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let app; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/i18n-routing-fallback/', + output: 'server', + outDir: './dist/i18n-routing-fallback', + build: { + client: './dist/i18n-routing-fallback/client', + server: './dist/i18n-routing-fallback/server', + }, + adapter: testAdapter(), + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + routing: { + prefixDefaultLocale: false, + fallbackType: 'rewrite', + }, + fallback: { + fr: 'en', + }, + }, + }); + await fixture.build(); + app = await fixture.loadTestAdapterApp(); + }); + + it('should correctly rewrite to en', async () => { + const request = new Request('http://example.com/fr'); + const response = await app.render(request); + assert.equal(response.status, 200); + const html = await response.text(); + assert.match(html, /Hello/); + }); +}); diff --git a/packages/astro/test/preact-component.test.js b/packages/astro/test/preact-component.test.js index e8c89d5bf..f5b5c7233 100644 --- a/packages/astro/test/preact-component.test.js +++ b/packages/astro/test/preact-component.test.js @@ -100,4 +100,44 @@ describe('Preact component', () => { assert.notEqual(sigs1.count, undefined); assert.equal(sigs1.count, sigs2.count); }); + + it('Can use signals in array', async () => { + const html = await fixture.readFile('/signals/index.html'); + const $ = cheerio.load(html); + const element = $('.preact-signal-array'); + assert.equal(element.length, 1); + + const sigs1Raw = $($('astro-island')[2]).attr('data-preact-signals'); + + const sigs1 = JSON.parse(sigs1Raw); + + assert.deepEqual(sigs1, { + signalsArray: [ + ['p0', 1], + ['p0', 2], + ['p1', 4], + ], + }); + + assert.equal(element.find('h1').text(), "I'm not a signal 12345"); + assert.equal(element.find('p').text(), '1-1-2'); + }); + + it('Can use signals in object', async () => { + const html = await fixture.readFile('/signals/index.html'); + const $ = cheerio.load(html); + const element = $('.preact-signal-object'); + assert.equal(element.length, 1); + + const sigs1Raw = $($('astro-island')[3]).attr('data-preact-signals'); + + const sigs1 = JSON.parse(sigs1Raw); + + assert.deepEqual(sigs1, { + signalsObject: [['p0', 'counter']], + }); + + assert.equal(element.find('h1').text(), 'I am a title'); + assert.equal(element.find('p').text(), '1'); + }); }); diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.js index 7839e7d33..10d7c70d2 100644 --- a/packages/astro/test/rewrite.test.js +++ b/packages/astro/test/rewrite.test.js @@ -104,7 +104,7 @@ describe('Dev rewrite, trailing slash -> never, with base', () => { }); it('should rewrite to the homepage', async () => { - const html = await fixture.fetch('/foo').then((res) => res.text()); + const html = await fixture.fetch('/base/foo').then((res) => res.text()); const $ = cheerioLoad(html); assert.equal($('h1').text(), 'Index'); diff --git a/packages/astro/test/units/actions/form-data-to-object.test.js b/packages/astro/test/units/actions/form-data-to-object.test.js index 136909305..e9f52a13f 100644 --- a/packages/astro/test/units/actions/form-data-to-object.test.js +++ b/packages/astro/test/units/actions/form-data-to-object.test.js @@ -192,4 +192,22 @@ describe('formDataToObject', () => { assert.equal(res.files instanceof Array, true); assert.deepEqual(res.files, [file1, file2]); }); + + it('should allow object passthrough when chaining .passthrough() on root object', () => { + const formData = new FormData(); + formData.set('expected', '42'); + formData.set('unexpected', '42'); + + const input = z + .object({ + expected: z.number(), + }) + .passthrough(); + + const res = formDataToObject(formData, input); + assert.deepEqual(res, { + expected: 42, + unexpected: '42', + }); + }); }); |