diff --git a/.changeset/chubby-plums-rule.md b/.changeset/chubby-plums-rule.md new file mode 100644 index 000000000000..88a5a0bf297b --- /dev/null +++ b/.changeset/chubby-plums-rule.md @@ -0,0 +1,9 @@ +--- +'@astrojs/internal-helpers': minor +--- + +Added a new entry point called `/request`, which exposes utilities to work with the `Request` type: +- `getFirstForwardedValue`: retrieves the first value of a multi-value header. +- `isValidIpAddress`: checks whether a string contains only characters valid in IPv4/IPv6 addresses. +- `getValidatedIpFromHeader`: extracts the first value from a header and validates it as an IP address. +- `getClientIpAddress`: retrieves and validates the first IP from the `x-forwarded-for` header. diff --git a/.changeset/clean-planets-flow.md b/.changeset/clean-planets-flow.md index 395589d4f9a8..40804e46970e 100644 --- a/.changeset/clean-planets-flow.md +++ b/.changeset/clean-planets-flow.md @@ -2,4 +2,4 @@ 'astro': patch --- -Improves rendering by preserving `hidden="until-found"` value in attribues +Improves rendering by preserving `hidden="until-found"` value in attributes diff --git a/.changeset/fix-envprefix-secret-leak.md b/.changeset/fix-envprefix-secret-leak.md new file mode 100644 index 000000000000..68a9f3324984 --- /dev/null +++ b/.changeset/fix-envprefix-secret-leak.md @@ -0,0 +1,24 @@ +--- +'astro': patch +--- + +Prevents `vite.envPrefix` misconfiguration from exposing `access: "secret"` environment variables in client-side bundles. Astro now throws a clear error at startup if any `vite.envPrefix` entry matches a variable declared with `access: "secret"` in `env.schema`. + +For example, the following configuration will throw an error for `API_SECRET` because it's defined as `secret` its name matches `['PUBLIC_', 'API_']` defined in `env.schema`: + +```js +// astro.config.mjs +import { defineConfig } from "astro/config"; + +export default defineConfig({ + env: { + schema: { + API_SECRET: envField.string({ context: 'server', access: 'secret', optional: true }), + API_URL: envField.string({ context: 'server', access: 'public', optional: true }), + } + }, + vite: { + envPrefix: ['PUBLIC_', 'API_'], + }, +}) +``` diff --git a/.changeset/full-poems-divide.md b/.changeset/full-poems-divide.md index 8abe7fb50c0d..a778d93c242b 100644 --- a/.changeset/full-poems-divide.md +++ b/.changeset/full-poems-divide.md @@ -3,4 +3,4 @@ 'astro': patch --- -Fixes a bug where the Astro, with the Cloudlfare integration, couldn't correctly serve certain routes in the development server. +Fixes a bug where the Astro, with the Cloudflare integration, couldn't correctly serve certain routes in the development server. diff --git a/.changeset/green-clowns-change.md b/.changeset/green-clowns-change.md new file mode 100644 index 000000000000..e43019f38765 --- /dev/null +++ b/.changeset/green-clowns-change.md @@ -0,0 +1,7 @@ +--- +'@astrojs/cloudflare': patch +'@astrojs/vercel': patch +'astro': patch +--- + +Fixes an issue where the computed `clientAddress` was incorrect in cases of a Request header with multiple values. The `clientAddress` is now also validated to contain only characters valid in IP addresses, rejecting injection payloads. diff --git a/.changeset/harden-merge-responses-framing.md b/.changeset/harden-merge-responses-framing.md new file mode 100644 index 000000000000..f3f30e1ad1b2 --- /dev/null +++ b/.changeset/harden-merge-responses-framing.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Hardens error page response merging to ensure framing headers from the original response are not carried over to the rendered error page diff --git a/.changeset/harden-node-server-defaults.md b/.changeset/harden-node-server-defaults.md new file mode 100644 index 000000000000..3a227f9c8ddf --- /dev/null +++ b/.changeset/harden-node-server-defaults.md @@ -0,0 +1,20 @@ +--- +'astro': patch +'@astrojs/node': minor +--- + +Adds a new `bodySizeLimit` option to the `@astrojs/node` adapter + +You can now configure a maximum allowed request body size for your Node.js standalone server. The default limit is 1 GB. Set the value in bytes, or pass `0` to disable the limit entirely: + +```js +import node from '@astrojs/node'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + adapter: node({ + mode: 'standalone', + bodySizeLimit: 1024 * 1024 * 100, // 100 MB + }), +}); +``` diff --git a/.changeset/moody-owls-refuse.md b/.changeset/moody-owls-refuse.md index f3fcdf20bb43..4c08f1fd04b3 100644 --- a/.changeset/moody-owls-refuse.md +++ b/.changeset/moody-owls-refuse.md @@ -2,4 +2,4 @@ '@astrojs/cloudflare': patch --- -Removes unneccessary warning about sharp from being printed at start of dev server and build +Removes unnecessary warning about sharp from being printed at start of dev server and build diff --git a/.changeset/normalize-backslash-pathname.md b/.changeset/normalize-backslash-pathname.md new file mode 100644 index 000000000000..8484143ed228 --- /dev/null +++ b/.changeset/normalize-backslash-pathname.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Hardens URL pathname normalization to consistently handle backslash characters after decoding, ensuring middleware and router see the same canonical pathname diff --git a/.changeset/normalize-dotfile-pathname.md b/.changeset/normalize-dotfile-pathname.md new file mode 100644 index 000000000000..57f29faee108 --- /dev/null +++ b/.changeset/normalize-dotfile-pathname.md @@ -0,0 +1,5 @@ +--- +'@astrojs/node': patch +--- + +Normalizes static file paths before evaluating dotfile access rules for improved consistency diff --git a/.changeset/social-kings-swim.md b/.changeset/social-kings-swim.md index 778e12f63253..47db520798a3 100644 --- a/.changeset/social-kings-swim.md +++ b/.changeset/social-kings-swim.md @@ -2,4 +2,4 @@ 'astro': patch --- -Fixes an issue where the internal perfomance timers weren't correctly updated to reflect new build pipeline. +Fixes an issue where the internal performance timers weren't correctly updated to reflect new build pipeline. diff --git a/.changeset/warm-pens-glow.md b/.changeset/warm-pens-glow.md new file mode 100644 index 000000000000..0c40ed0af75a --- /dev/null +++ b/.changeset/warm-pens-glow.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes form actions incorrectly auto-executing during error page rendering. When an error page (e.g. 404) is rendered, form actions from the original request are no longer executed, since the full request handling pipeline is not active. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 984929a52f32..a6c860cd0ac6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -116,7 +116,7 @@ pnpm run test pnpm run test:match "$STRING_MATCH" # run tests on another package # (example - `pnpm --filter @astrojs/rss run test` runs `packages/astro-rss/test/rss.test.js`) -pnpm --filter $STRING_MATCH run test +pnpm --filter "$STRING_MATCH" run test ``` Most tests use [`mocha`](https://mochajs.org) as the test runner. We're slowly migrating to use [`node:test`](https://nodejs.org/api/test.html) instead through the custom [`astro-scripts test`](./scripts/cmd/test.js) command. For packages that use `node:test`, you can run these commands in their directories: @@ -165,7 +165,7 @@ node --test --test-only test/astro-basic.test.js #### Debugging tests in CI -There might be occasions where some tests fail in certain CI runs due to some timeout issue. If this happens, it will be very difficult to understand which file cause the timeout. That's caused by come quirks of the Node.js test runner combined with our architecture. +There might be occasions where some tests fail in certain CI runs due to some timeout issue. If this happens, it will be very difficult to understand which file cause the timeout. That's caused by some quirks of the Node.js test runner combined with our architecture. To understand which file causes the issue, you can modify the `test` script inside the `package.json` by adding the `--parallel` option: @@ -176,7 +176,7 @@ To understand which file causes the issue, you can modify the `test` script insi } ``` -Save the change and **push it** to your PR. This change will make the test CI slower, but it will allow to see which files causes the timeout. Once you fixed the issue **revert the change and push it**. +Save the change and **push it** to your PR. This change will make the test CI slower, but it will allow to see which files cause the timeout. Once you fixed the issue **revert the change and push it**. #### E2E tests @@ -194,7 +194,7 @@ pnpm run test:e2e:match "$STRING_MATCH" Any tests for `astro build` output should use the main `mocha` tests rather than E2E - these tests will run faster than having Playwright start the `astro preview` server. -If a test needs to validate what happens on the page after it's loading in the browser, that's a perfect use for E2E dev server tests, i.e. to verify that hot-module reloading works in `astro dev` or that components were client hydrated and are interactive. +If a test needs to validate what happens on the page after it's loaded in the browser, that's a perfect use for E2E dev server tests, i.e. to verify that hot-module reloading works in `astro dev` or that components were client hydrated and are interactive. #### Creating tests @@ -203,8 +203,8 @@ When creating new tests, it's best to reference other existing test files and re - When re-using a fixture multiple times with different configurations, you should also configure unique `outDir`, `build.client`, and `build.server` values so the build output runtime isn't cached and shared by ESM between test runs. > [!IMPORTANT] -> If tests start to fail for no apparent reason, the first thing to look at the `outDir` configuration. As build cache artifacts between runs, different tests might end up sharing some of the emitted modules. -> To avoid this possible overlap, **make sure to add a custom `outDir` to your test case** +> If tests start to fail for no apparent reason, the first thing to look at the `outDir` configuration. As build caches artifacts between runs, different tests might end up sharing some of the emitted modules. +> To avoid this possible overlap, **make sure to add a custom `outDir` to your test case**. > > ```js > await loadFixture({ @@ -259,7 +259,7 @@ To run only a specific benchmark on CI, add its name after the command in your c ## For maintainers -This paragraph provides some guidance to the maintainers of the monorepo. The guidelines explained here aren't necessarily followed by other repositories of the same GitHub organisation. +This paragraph provides some guidance to the maintainers of the monorepo. The guidelines explained here aren't necessarily followed by other repositories of the GitHub organisation. ### Issue triaging workflow @@ -301,7 +301,7 @@ The Astro project has five levels of priority to issues, where `p5` is the highe - `p4`: the bug impacts _many_ Astro projects, it doesn't have a workaround but Astro is still stable/usable. - `p3`: any bug that doesn't fall in the `p4` or `p5` category. If the documentation doesn't cover the case reported by the user, it's useful to initiate a discussion via the `"needs discussion"` label. Seek opinions from OP and other maintainers. -- `p2`: all the bugs that have workarounds. +- `p2`: all bugs that have workarounds. - `p1`: very minor bug, that impacts a small amount of users. Sometimes it's an edge case and it's easy to fix. Very useful if you want to assign the fix to a first-time contributor. > [!IMPORTANT] @@ -312,7 +312,7 @@ Assigning labels isn't always easy and many times the distinction between the di - When assigning a `p2`, **always** add a comment that explains the workaround. If a workaround isn't provided, ping the person that assigned the label and ask them to provide one. - Astro has **many** features, but there are some that have a larger impact than others: development server, build command, HMR (TBD, we don't have a page that explains expectations of HMR in Astro), **evident** regressions in performance. - In case the number of reactions of an issue grows, the number of users affected grows, or a discussion uncovers some insights that weren't clear before, it's OK to change the priority of the issue. The maintainer **should** provide an explanation when assigning a different label. - As with any other contribution, triaging is voluntary and best-efforts. We welcome and appreciate all the help you're happy to give (including reading this!) and nothing more. If you are not confident about an issue, you are welcome to leave an issue untriaged for someone who would have more context, or to bring it to their attention. + As with any other contribution, triaging is voluntary and best-effort. We welcome and appreciate all the help you're happy to give (including reading this!) and nothing more. If you are not confident about an issue, you are welcome to leave an issue untriaged for someone who would have more context, or to bring it to their attention. ### Preview releases @@ -348,7 +348,7 @@ Understanding in which environment code runs, and at which stage in the process, To make it easier to test code, try decoupling **business logic** from **infrastructure**: -- **Infrastucture** is code that depends on external systems and/or requires aspecial environment to run. For example: DB calls, file system, randomness etc... +- **Infrastructure** is code that depends on external systems and/or requires a special environment to run. For example: DB calls, file system, randomness etc... - **Business logic** (or _core logic_ or _domain_) is the rest. It's pure logic that's easy to run from anywhere. That means avoiding side-effects by making external dependencies explicit. This often means passing more things as arguments. diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 6d0cc9e26a8f..69b69660481c 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -45,7 +45,7 @@ For example: "This is clean code" is a subjective point and should have limited In contrast: "Tabs are more accessible than spaces" is an objective point and should be strongly considered in a theoretical style discussion on tabs vs. spaces. (Fred: Believe me, I write this as someone who personally prefers spaces over tabs in my own code!) -Sometimes, not everyone will agree on style changes and 100% consensus is impossible. This is a condition commonly referred to as bike-shedding. If consensus can not be reached, a simple majority vote among core contributors (L3) will suffice. +Sometimes, not everyone will agree on style changes and 100% consensus is impossible. This is a condition commonly referred to as bike-shedding. If consensus cannot be reached, a simple majority vote among core contributors (L3) will suffice. _Note: This process is new, we are still figuring it out! This process will be moved into GOVERNANCE.md when finalized._ diff --git a/examples/blog/README.md b/examples/blog/README.md index 1e88fbf226fc..4307d60ba3c9 100644 --- a/examples/blog/README.md +++ b/examples/blog/README.md @@ -24,7 +24,7 @@ Features: - ✅ Minimal styling (make it your own!) - ✅ 100/100 Lighthouse performance -- ✅ SEO-friendly with canonical URLs and OpenGraph data +- ✅ SEO-friendly with canonical URLs and Open Graph data - ✅ Sitemap support - ✅ RSS Feed support - ✅ Markdown & MDX support diff --git a/examples/ssr/src/components/AddToCart.svelte b/examples/ssr/src/components/AddToCart.svelte index 9e6c8ba866e8..47136c1dcaa8 100644 --- a/examples/ssr/src/components/AddToCart.svelte +++ b/examples/ssr/src/components/AddToCart.svelte @@ -34,7 +34,7 @@ button:hover { transform:scale(1.1); } -.pretext { +.pre-text { color:#fff; background:#0652DD; position:absolute; @@ -50,5 +50,5 @@ button:hover { } diff --git a/examples/starlog/src/components/Header.astro b/examples/starlog/src/components/Header.astro index bbdaf1e70c10..e74f30f2aff8 100644 --- a/examples/starlog/src/components/Header.astro +++ b/examples/starlog/src/components/Header.astro @@ -42,7 +42,7 @@ import { SiteTitle } from '../consts'; diff --git a/examples/starlog/src/components/SEO.astro b/examples/starlog/src/components/SEO.astro index 8816bd7bee27..2ceebb4c9d6d 100644 --- a/examples/starlog/src/components/SEO.astro +++ b/examples/starlog/src/components/SEO.astro @@ -69,7 +69,7 @@ function normalizeImageUrl(image: string | ImageMetadata) { - + diff --git a/packages/astro-rss/CHANGELOG.md b/packages/astro-rss/CHANGELOG.md index ae1e8cd21725..2cc092daad9c 100644 --- a/packages/astro-rss/CHANGELOG.md +++ b/packages/astro-rss/CHANGELOG.md @@ -250,7 +250,7 @@ ### Patch Changes -- [#6614](https://github.com/withastro/astro/pull/6614) [`b1b9b1390`](https://github.com/withastro/astro/commit/b1b9b1390f95c6ae91389eba55f7563b911bccc7) Thanks [@aivarsliepa](https://github.com/aivarsliepa)! - Fixes `RSSOptions` type error when using `strictest` Typescript tsconfig +- [#6614](https://github.com/withastro/astro/pull/6614) [`b1b9b1390`](https://github.com/withastro/astro/commit/b1b9b1390f95c6ae91389eba55f7563b911bccc7) Thanks [@aivarsliepa](https://github.com/aivarsliepa)! - Fixes `RSSOptions` type error when using `strictest` TypeScript tsconfig ## 2.3.1 diff --git a/packages/astro-rss/README.md b/packages/astro-rss/README.md index de897d476029..7a660f46e3d1 100644 --- a/packages/astro-rss/README.md +++ b/packages/astro-rss/README.md @@ -49,7 +49,7 @@ An `RSSFeedItem` is a single item in the list of items in your feed. An example ```js const item = { title: 'Alpha Centauri: so close you can touch it', - link: '/blog/alpha-centuari', + link: '/blog/alpha-centauri', pubDate: new Date('2023-06-04'), description: 'Alpha Centauri is a triple star system, containing Proxima Centauri, the closest star to our sun at only 4.24 light-years away.', @@ -116,7 +116,7 @@ An object that defines the `title` and `url` of the original feed for items that ```js const item = { title: 'Alpha Centauri: so close you can touch it', - link: '/blog/alpha-centuari', + link: '/blog/alpha-centauri', pubDate: new Date('2023-06-04'), description: 'Alpha Centauri is a triple star system, containing Proxima Centauri, the closest star to our sun at only 4.24 light-years away.', diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.js index 63c91887d283..a52caf45c361 100644 --- a/packages/astro-rss/test/rss.test.js +++ b/packages/astro-rss/test/rss.test.js @@ -105,7 +105,7 @@ describe('getRssString', () => { assertXmlDeepEqual(str, validXmlWithContentResult); }); - it('should generate on valid RSSFeedItem array with missing date', async () => { + it('should generate on valid RSSFeedItem array that is missing date', async () => { const str = await getRssString({ title, description, diff --git a/packages/astro/CHANGELOG-v1.md b/packages/astro/CHANGELOG-v1.md index 1ebd4a598179..57c7c7a7bb5b 100644 --- a/packages/astro/CHANGELOG-v1.md +++ b/packages/astro/CHANGELOG-v1.md @@ -815,7 +815,7 @@ ### Patch Changes -- [#4768](https://github.com/withastro/astro/pull/4768) [`9a59e24e0`](https://github.com/withastro/astro/commit/9a59e24e0250617333c1a0fd89b7d52fd1c829de) Thanks [@matthewp](https://github.com/matthewp)! - nsure before-hydration is only loaded when used +- [#4768](https://github.com/withastro/astro/pull/4768) [`9a59e24e0`](https://github.com/withastro/astro/commit/9a59e24e0250617333c1a0fd89b7d52fd1c829de) Thanks [@matthewp](https://github.com/matthewp)! - Ensure before-hydration is only loaded when used - [#4759](https://github.com/withastro/astro/pull/4759) [`fc885eaea`](https://github.com/withastro/astro/commit/fc885eaea1f08429599c0ab4697ab6382f3d7fa4) Thanks [@matthewp](https://github.com/matthewp)! - Read jsxImportSource from tsconfig diff --git a/packages/astro/CHANGELOG-v2.md b/packages/astro/CHANGELOG-v2.md index d24276f25e8c..c3dd4e4519dd 100644 --- a/packages/astro/CHANGELOG-v2.md +++ b/packages/astro/CHANGELOG-v2.md @@ -268,7 +268,7 @@ - [#7786](https://github.com/withastro/astro/pull/7786) [`188eeddd4`](https://github.com/withastro/astro/commit/188eeddd47a61e04639670496924c37866180749) Thanks [@matthewp](https://github.com/matthewp)! - Execute scripts when navigating to a new page. - When navigating to an new page with client-side navigation, scripts are executed (and re-executed) so that any new scripts on the incoming page are run and the DOM can be updated. + When navigating to a new page with client-side navigation, scripts are executed (and re-executed) so that any new scripts on the incoming page are run and the DOM can be updated. However, `type=module` scripts never re-execute in Astro, and will not do so in client-side routing. To support cases where you want to modify the DOM, a new `astro:load` event listener been added: @@ -550,7 +550,7 @@ ### Patch Changes -- [#7527](https://github.com/withastro/astro/pull/7527) [`9e2426f75`](https://github.com/withastro/astro/commit/9e2426f75637a6318961f483de90b635f3fdadeb) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Default registry logic to fallback to NPM if registry command fails (sorry, Bun users!) +- [#7527](https://github.com/withastro/astro/pull/7527) [`9e2426f75`](https://github.com/withastro/astro/commit/9e2426f75637a6318961f483de90b635f3fdadeb) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Default registry logic to fall back to NPM if registry command fails (sorry, Bun users!) - [#7542](https://github.com/withastro/astro/pull/7542) [`cdc28326c`](https://github.com/withastro/astro/commit/cdc28326cf21f305924363e9c8c02ce54b6ff895) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Fix bug when using `define:vars` with a `style` object @@ -1087,7 +1087,7 @@ ### Patch Changes -- [#7009](https://github.com/withastro/astro/pull/7009) [`1d4db68e6`](https://github.com/withastro/astro/commit/1d4db68e64b7c3faf8863bf67f8332aa28e2f34b) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix types from `astro/client` not working properly due to `client-base.d.ts` being an non-ambient declaration file +- [#7009](https://github.com/withastro/astro/pull/7009) [`1d4db68e6`](https://github.com/withastro/astro/commit/1d4db68e64b7c3faf8863bf67f8332aa28e2f34b) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix types from `astro/client` not working properly due to `client-base.d.ts` being a non-ambient declaration file - [#7010](https://github.com/withastro/astro/pull/7010) [`e9f0dd9b4`](https://github.com/withastro/astro/commit/e9f0dd9b473c4793c958a6c81e743fd9b02b4f64) Thanks [@ematipico](https://github.com/ematipico)! - Call `next()` without return anything should work, with a warning @@ -1361,7 +1361,7 @@ - [#6675](https://github.com/withastro/astro/pull/6675) [`1f783e320`](https://github.com/withastro/astro/commit/1f783e32075c20b13063599696644f5d47b75d8d) Thanks [@matthewp](https://github.com/matthewp)! - Prevent frontmatter errors from crashing the dev server -- [#6688](https://github.com/withastro/astro/pull/6688) [`2e92e9aa9`](https://github.com/withastro/astro/commit/2e92e9aa976735c3ddb647152bb9c4850136e386) Thanks [@JohannesKlauss](https://github.com/JohannesKlauss)! - Add a additional check for `null` on the `req.body` check in `NodeApp.render`. +- [#6688](https://github.com/withastro/astro/pull/6688) [`2e92e9aa9`](https://github.com/withastro/astro/commit/2e92e9aa976735c3ddb647152bb9c4850136e386) Thanks [@JohannesKlauss](https://github.com/JohannesKlauss)! - Add an additional check for `null` on the `req.body` check in `NodeApp.render`. - [#6578](https://github.com/withastro/astro/pull/6578) [`adecda7d6`](https://github.com/withastro/astro/commit/adecda7d6009793c5d20519a997e3b7afb08ad57) Thanks [@wulinsheng123](https://github.com/wulinsheng123)! - add new flag with open for dev and preview @@ -1755,8 +1755,8 @@ - [#6052](https://github.com/withastro/astro/pull/6052) [`9793f19ec`](https://github.com/withastro/astro/commit/9793f19ecd4e64cbf3140454fe52aeee2c22c8c9) Thanks [@mayank99](https://github.com/mayank99)! - Error overlay will now show the error's `cause` if available. -- [#6070](https://github.com/withastro/astro/pull/6070) [`f91615f5c`](https://github.com/withastro/astro/commit/f91615f5c04fde36f115dad9110dd75254efd61d) Thanks [@AirBorne04](https://github.com/AirBorne04)! - \* safe guard against TextEncode.encode(HTMLString) [errors on vercel edge] - - safe guard against html.replace when html is undefined +- [#6070](https://github.com/withastro/astro/pull/6070) [`f91615f5c`](https://github.com/withastro/astro/commit/f91615f5c04fde36f115dad9110dd75254efd61d) Thanks [@AirBorne04](https://github.com/AirBorne04)! - \* safeguard against TextEncode.encode(HTMLString) [errors on vercel edge] + - safeguard against html.replace when html is undefined - [#6064](https://github.com/withastro/astro/pull/6064) [`2fb72c887`](https://github.com/withastro/astro/commit/2fb72c887f71c0a69ab512870d65b8c867774766) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Apply MDX `components` export when rendering as a content collection entry @@ -1853,7 +1853,7 @@ } ``` - This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or reype. + This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or rehype. - [#5891](https://github.com/withastro/astro/pull/5891) [`05caf445d`](https://github.com/withastro/astro/commit/05caf445d4d2728f1010aeb2179a9e756c2fd17d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Remove deprecated Markdown APIs from Astro v0.X. This includes `getHeaders()`, the `.astro` property for layouts, and the `rawContent()` and `compiledContent()` error messages for MDX. @@ -2481,7 +2481,7 @@ } ``` - This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or reype. + This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or rehype. - [#5728](https://github.com/withastro/astro/pull/5728) [`8fb28648f`](https://github.com/withastro/astro/commit/8fb28648f66629741cb976bfe34ccd9d8f55661e) Thanks [@natemoo-re](https://github.com/natemoo-re)! - The previously experimental features `--experimental-error-overlay` and `--experimental-prerender`, both added in v1.7.0, are now the default. diff --git a/packages/astro/CHANGELOG-v3.md b/packages/astro/CHANGELOG-v3.md index 06133bd50189..8c4e914fe0f5 100644 --- a/packages/astro/CHANGELOG-v3.md +++ b/packages/astro/CHANGELOG-v3.md @@ -146,7 +146,7 @@ - [#9121](https://github.com/withastro/astro/pull/9121) [`f4efd1c80`](https://github.com/withastro/astro/commit/f4efd1c808476c7e60fe00fcfb86276cf14fee79) Thanks [@peng](https://github.com/peng)! - Adds a warning if `astro add` fetches a package but returns a non-404 status -- [#9142](https://github.com/withastro/astro/pull/9142) [`7d55cf68d`](https://github.com/withastro/astro/commit/7d55cf68d89cb46bfb89a109b09af61be8431c89) Thanks [@ematipico](https://github.com/ematipico)! - Consistely emit fallback routes in the correct folders. +- [#9142](https://github.com/withastro/astro/pull/9142) [`7d55cf68d`](https://github.com/withastro/astro/commit/7d55cf68d89cb46bfb89a109b09af61be8431c89) Thanks [@ematipico](https://github.com/ematipico)! - Consistently emit fallback routes in the correct folders. - [#9119](https://github.com/withastro/astro/pull/9119) [`306781795`](https://github.com/withastro/astro/commit/306781795d5f4b755bbdf650a937f1f3c00030bd) Thanks [@ematipico](https://github.com/ematipico)! - Fix a flaw in the i18n fallback logic, where the routes didn't preserve their metadata, such as hoisted scripts @@ -166,7 +166,7 @@ ### Patch Changes -- [#9085](https://github.com/withastro/astro/pull/9085) [`fc66ecff1`](https://github.com/withastro/astro/commit/fc66ecff18a20dd436026cb8e75bcc8b5ab0e681) Thanks [@ematipico](https://github.com/ematipico)! - When redirecting to the default root locale, Astro middleare should take into consideration the value of `trailingSlash` +- [#9085](https://github.com/withastro/astro/pull/9085) [`fc66ecff1`](https://github.com/withastro/astro/commit/fc66ecff18a20dd436026cb8e75bcc8b5ab0e681) Thanks [@ematipico](https://github.com/ematipico)! - When redirecting to the default root locale, Astro middleware should take into consideration the value of `trailingSlash` - [#9067](https://github.com/withastro/astro/pull/9067) [`c6e449c5b`](https://github.com/withastro/astro/commit/c6e449c5b3e6e994b362b9ce441c8a1a81129f23) Thanks [@danielhajduk](https://github.com/danielhajduk)! - Fixes display of debug messages when using the `--verbose` flag @@ -403,7 +403,7 @@ ### Patch Changes -- [#9016](https://github.com/withastro/astro/pull/9016) [`1ecc9aa32`](https://github.com/withastro/astro/commit/1ecc9aa3240b79a3879b1329aa4f671d80e87649) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Add ability to "Click to go editor" on auditted elements in the dev overlay +- [#9016](https://github.com/withastro/astro/pull/9016) [`1ecc9aa32`](https://github.com/withastro/astro/commit/1ecc9aa3240b79a3879b1329aa4f671d80e87649) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Add ability to "Click to go editor" on audited elements in the dev overlay - [#9029](https://github.com/withastro/astro/pull/9029) [`29b83e9e4`](https://github.com/withastro/astro/commit/29b83e9e4b906cc0b5d92fae854fb350fc2be7c8) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Use UInt8Array instead of Buffer for both the input and return values of the `transform()` hook of the Image Service API to ensure compatibility with non-Node runtimes. @@ -873,7 +873,7 @@ - [#8484](https://github.com/withastro/astro/pull/8484) [`78b82bb39`](https://github.com/withastro/astro/commit/78b82bb3929bee5d8d9bd32d65374956ddb05859) Thanks [@bb010g](https://github.com/bb010g)! - fix(astro): add support for `src/content/config.mts` files -- [#8504](https://github.com/withastro/astro/pull/8504) [`5e1099f68`](https://github.com/withastro/astro/commit/5e1099f686abcc7026bd4fa74727f3b311c6d6d6) Thanks [@ematipico](https://github.com/ematipico)! - Minify the HTML of the redicts emitted during the build. +- [#8504](https://github.com/withastro/astro/pull/8504) [`5e1099f68`](https://github.com/withastro/astro/commit/5e1099f686abcc7026bd4fa74727f3b311c6d6d6) Thanks [@ematipico](https://github.com/ematipico)! - Minify the HTML of the redirects emitted during the build. - [#8480](https://github.com/withastro/astro/pull/8480) [`644825845`](https://github.com/withastro/astro/commit/644825845c11c8d100a9b0d16b69a23c165c529e) Thanks [@yamanoku](https://github.com/yamanoku)! - Do not add type="text/css" to inline style tag diff --git a/packages/astro/CHANGELOG-v4.md b/packages/astro/CHANGELOG-v4.md index 447c8b3d0ed5..32abbf44eba1 100644 --- a/packages/astro/CHANGELOG-v4.md +++ b/packages/astro/CHANGELOG-v4.md @@ -4,7 +4,7 @@ - [#12542](https://github.com/withastro/astro/pull/12542) [`65e50eb`](https://github.com/withastro/astro/commit/65e50eb7b6d7b10a193bba7d292804ac0e55be18) Thanks [@kadykov](https://github.com/kadykov)! - Fix JPEG image size determination -- [#12525](https://github.com/withastro/astro/pull/12525) [`cf0d8b0`](https://github.com/withastro/astro/commit/cf0d8b08a0f16bba7310d1a92c82b5a276682e8c) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where with `i18n` enabled, Astro couldn't render the `404.astro` component for non-existent routes. +- [#12525](https://github.com/withastro/astro/pull/12525) [`cf0d8b0`](https://github.com/withastro/astro/commit/cf0d8b08a0f16bba7310d1a92c82b5a276682e8c) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where with `i18n` enabled, Astro couldn't render the `404.astro` component for nonexistent routes. ## 4.16.15 @@ -770,7 +770,7 @@ - [#11360](https://github.com/withastro/astro/pull/11360) [`a79a8b0`](https://github.com/withastro/astro/commit/a79a8b0230b06ed32ce1802f2a5f84a6cf92dbe7) Thanks [@ascorbic](https://github.com/ascorbic)! - Adds a new [`injectTypes()` utility](https://docs.astro.build/en/reference/integrations-reference/#injecttypes-options) to the Integration API and refactors how type generation works - Use `injectTypes()` in the `astro:config:done` hook to inject types into your user's project by adding a new a `*.d.ts` file. + Use `injectTypes()` in the `astro:config:done` hook to inject types into your user's project by adding a new `*.d.ts` file. The `filename` property will be used to generate a file at `/.astro/integrations//.d.ts` and must end with `".d.ts"`. @@ -1236,7 +1236,7 @@ ``` - You may also construct form action URLs using string concatenation, or by using the `URL()` constructor, with the an action's `.queryString` property: + You may also construct form action URLs using string concatenation, or by using the `URL()` constructor, with an action's `.queryString` property: ```astro --- @@ -1589,7 +1589,7 @@ - [#11352](https://github.com/withastro/astro/pull/11352) [`a55ee02`](https://github.com/withastro/astro/commit/a55ee0268e1ca22597e9b5e6d1f24b4f28ad978b) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where the rewrites didn't update the status code when using manual i18n routing. -- [#11388](https://github.com/withastro/astro/pull/11388) [`3a223b4`](https://github.com/withastro/astro/commit/3a223b4811708cc93ebb27706118c1723e1fc013) Thanks [@mingjunlu](https://github.com/mingjunlu)! - Adjusts the color of punctuations in error overlay. +- [#11388](https://github.com/withastro/astro/pull/11388) [`3a223b4`](https://github.com/withastro/astro/commit/3a223b4811708cc93ebb27706118c1723e1fc013) Thanks [@mingjunlu](https://github.com/mingjunlu)! - Adjusts the color of punctuation in error overlay. - [#11369](https://github.com/withastro/astro/pull/11369) [`e6de11f`](https://github.com/withastro/astro/commit/e6de11f4a941e29123da3714e5b8f17d25744f0f) Thanks [@bluwy](https://github.com/bluwy)! - Fixes attribute rendering for non-boolean attributes with boolean values @@ -2571,7 +2571,7 @@ } ``` - This is a backwards compatible change and your your existing dev toolbar apps will continue to function. However, we encourage you to build your apps with the new helpers, following the [updated Dev Toolbar API documentation](https://docs.astro.build/en/reference/dev-toolbar-app-reference/). + This is a backwards compatible change and your existing dev toolbar apps will continue to function. However, we encourage you to build your apps with the new helpers, following the [updated Dev Toolbar API documentation](https://docs.astro.build/en/reference/dev-toolbar-app-reference/). - [#10734](https://github.com/withastro/astro/pull/10734) [`6fc4c0e`](https://github.com/withastro/astro/commit/6fc4c0e420da7629b4cfc28ee7efce1d614447be) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Astro will now automatically check for updates when you run the dev server. If a new version is available, a message will appear in the terminal with instructions on how to update. Updates will be checked once per 10 days, and the message will only appear if the project is multiple versions behind the latest release. @@ -2898,7 +2898,7 @@ - [#10438](https://github.com/withastro/astro/pull/10438) [`5b48cc0fc8383b0659a595afd3a6ee28b28779c3`](https://github.com/withastro/astro/commit/5b48cc0fc8383b0659a595afd3a6ee28b28779c3) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Generate Astro DB types when running `astro sync`. -- [#10456](https://github.com/withastro/astro/pull/10456) [`1900a8f9bc337f3a882178d1770e10ab67fab0ce`](https://github.com/withastro/astro/commit/1900a8f9bc337f3a882178d1770e10ab67fab0ce) Thanks [@martrapp](https://github.com/martrapp)! - Fixes an error when using `astro:transtions/client` without `` +- [#10456](https://github.com/withastro/astro/pull/10456) [`1900a8f9bc337f3a882178d1770e10ab67fab0ce`](https://github.com/withastro/astro/commit/1900a8f9bc337f3a882178d1770e10ab67fab0ce) Thanks [@martrapp](https://github.com/martrapp)! - Fixes an error when using `astro:transitions/client` without `` ## 4.5.4 @@ -3107,7 +3107,7 @@ - [#10343](https://github.com/withastro/astro/pull/10343) [`f973aa9110592fa9017bbe84387f22c24a6d7159`](https://github.com/withastro/astro/commit/f973aa9110592fa9017bbe84387f22c24a6d7159) Thanks [@ematipico](https://github.com/ematipico)! - Fixes some false positive in the dev toolbar a11y audits, by adding the `a` element to the list of interactive elements. -- [#10295](https://github.com/withastro/astro/pull/10295) [`fdd5bf277e5c1cfa30c1bd2ca123f4e90e8d09d9`](https://github.com/withastro/astro/commit/fdd5bf277e5c1cfa30c1bd2ca123f4e90e8d09d9) Thanks [@rossrobino](https://github.com/rossrobino)! - Adds a prefetch fallback when using the `experimental.clientPrerender` option. If prerendering fails, which can happen if [Chrome extensions block prerendering](https://developer.chrome.com/blog/speculation-rules-improvements#chrome-limits), it will fallback to prefetching the URL. This works by adding a `prefetch` field to the `speculationrules` script, but does not create an extra request. +- [#10295](https://github.com/withastro/astro/pull/10295) [`fdd5bf277e5c1cfa30c1bd2ca123f4e90e8d09d9`](https://github.com/withastro/astro/commit/fdd5bf277e5c1cfa30c1bd2ca123f4e90e8d09d9) Thanks [@rossrobino](https://github.com/rossrobino)! - Adds a prefetch fallback when using the `experimental.clientPrerender` option. If prerendering fails, which can happen if [Chrome extensions block prerendering](https://developer.chrome.com/blog/speculation-rules-improvements#chrome-limits), it will fall back to prefetching the URL. This works by adding a `prefetch` field to the `speculationrules` script, but does not create an extra request. ## 4.4.13 @@ -3307,15 +3307,15 @@ Although the incorrect first comparison is not a problem by itself, it could cause the algorithm to make the wrong decision. Depending on the other routes in the project, the sorting could perform just the last two comparisons and by transitivity infer the inverse of the third (`/blog/[...slug` > `/` > `/blog`), which is incorrect. - Now the algorithm doesn't have a special case for index pages and instead does the comparison soleley for rest parameter segments and their immediate parents, which is consistent with the transitivity property. + Now the algorithm doesn't have a special case for index pages and instead does the comparison solely for rest parameter segments and their immediate parents, which is consistent with the transitivity property. - [#10120](https://github.com/withastro/astro/pull/10120) [`787e6f52470cf07fb50c865948b2bc8fe45a6d31`](https://github.com/withastro/astro/commit/787e6f52470cf07fb50c865948b2bc8fe45a6d31) Thanks [@bluwy](https://github.com/bluwy)! - Updates and supports Vite 5.1 - [#10096](https://github.com/withastro/astro/pull/10096) [`227cd83a51bbd451dc223fd16f4cf1b87b8e44f8`](https://github.com/withastro/astro/commit/227cd83a51bbd451dc223fd16f4cf1b87b8e44f8) Thanks [@Fryuni](https://github.com/Fryuni)! - Fixes edge case on i18n fallback routes - Previously index routes deeply nested in the default locale, like `/some/nested/index.astro` could be mistaked as the root index for the default locale, resulting in an incorrect redirect on `/`. + Previously index routes deeply nested in the default locale, like `/some/nested/index.astro` could be mistaken as the root index for the default locale, resulting in an incorrect redirect on `/`. -- [#10112](https://github.com/withastro/astro/pull/10112) [`476b79a61165d0aac5e98459a4ec90762050a14b`](https://github.com/withastro/astro/commit/476b79a61165d0aac5e98459a4ec90762050a14b) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Renames the home Astro Devoolbar App to `astro:home` +- [#10112](https://github.com/withastro/astro/pull/10112) [`476b79a61165d0aac5e98459a4ec90762050a14b`](https://github.com/withastro/astro/commit/476b79a61165d0aac5e98459a4ec90762050a14b) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Renames the home Astro Devtoolbar App to `astro:home` - [#10117](https://github.com/withastro/astro/pull/10117) [`51b6ff7403c1223b1c399e88373075972c82c24c`](https://github.com/withastro/astro/commit/51b6ff7403c1223b1c399e88373075972c82c24c) Thanks [@hippotastic](https://github.com/hippotastic)! - Fixes an issue where `create astro`, `astro add` and `@astrojs/upgrade` would fail due to unexpected package manager CLI output. @@ -3379,7 +3379,7 @@ - [#9932](https://github.com/withastro/astro/pull/9932) [`9f0d89fa7e9e7c08c8600b0c49c2cce7489a7582`](https://github.com/withastro/astro/commit/9f0d89fa7e9e7c08c8600b0c49c2cce7489a7582) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a case where a warning was logged even when the feature `i18nDomains` wasn't enabled -- [#9907](https://github.com/withastro/astro/pull/9907) [`6c894af5ab79f290f4ff7feb68617a66e91febc1`](https://github.com/withastro/astro/commit/6c894af5ab79f290f4ff7feb68617a66e91febc1) Thanks [@ktym4a](https://github.com/ktym4a)! - Load 404.html on all non-existent paths on astro preview. +- [#9907](https://github.com/withastro/astro/pull/9907) [`6c894af5ab79f290f4ff7feb68617a66e91febc1`](https://github.com/withastro/astro/commit/6c894af5ab79f290f4ff7feb68617a66e91febc1) Thanks [@ktym4a](https://github.com/ktym4a)! - Load 404.html on all nonexistent paths on astro preview. ## 4.3.1 @@ -3733,7 +3733,7 @@ Enabling this feature overrides the default `prefetch` behavior globally to prerender links on the client according to your `prefetch` configuration. Instead of appending a `` tag to the head of the document or fetching the page with JavaScript, a ` + + diff --git a/packages/astro/test/fixtures/core-image-svg/src/pages/index.astro b/packages/astro/test/fixtures/core-image-svg/src/pages/index.astro index bff83cb9d5a4..7f341f4d934f 100644 --- a/packages/astro/test/fixtures/core-image-svg/src/pages/index.astro +++ b/packages/astro/test/fixtures/core-image-svg/src/pages/index.astro @@ -1,5 +1,5 @@ --- -import ChrevronRight from '~/assets/chevron-right.svg' +import ChevronRight from '~/assets/chevron-right.svg' --- @@ -7,7 +7,7 @@ import ChrevronRight from '~/assets/chevron-right.svg'
- +
diff --git a/packages/astro/test/fixtures/core-image/src/custom-endpoint.ts b/packages/astro/test/fixtures/core-image/src/custom-endpoint.ts index 22c32497b009..b94136d10289 100644 --- a/packages/astro/test/fixtures/core-image/src/custom-endpoint.ts +++ b/packages/astro/test/fixtures/core-image/src/custom-endpoint.ts @@ -1,3 +1,3 @@ export const GET = async () => { - return new Response("You fool! I'm not a image endpoint at all, I just return this!", { status: 200 }); + return new Response("You fool! I'm not an image endpoint at all, I just return this!", { status: 200 }); }; diff --git a/packages/astro/test/fixtures/integration-add-page-extension/src/pages/test.mjs b/packages/astro/test/fixtures/integration-add-page-extension/src/pages/test.mjs index b6bed7c564f3..2dd1a2cb0550 100644 --- a/packages/astro/test/fixtures/integration-add-page-extension/src/pages/test.mjs +++ b/packages/astro/test/fixtures/integration-add-page-extension/src/pages/test.mjs @@ -1,3 +1,3 @@ -// Convulted test case, rexport astro file from new `.mjs` page +// Convoluted test case, re-export astro file from new `.mjs` page import Test from '../components/test.astro'; export default Test; diff --git a/packages/astro/test/fixtures/markdown/src/pages/realworld.md b/packages/astro/test/fixtures/markdown/src/pages/realworld.md index 8ba6ea2d58f1..dde567ef60e4 100644 --- a/packages/astro/test/fixtures/markdown/src/pages/realworld.md +++ b/packages/astro/test/fixtures/markdown/src/pages/realworld.md @@ -76,7 +76,7 @@ The PHP above displays the tabs. The code below, very similarly, displays the ta ``` -By looping through the same repeater, we can get all the tabs out of the database, no problem. But we still have two problems: 1) linking the tab to the pane 2) Assigning the class of "active" so the Javascript is able to add and remove the CSS to reveal / hide the appropriate pane. +By looping through the same repeater, we can get all the tabs out of the database, no problem. But we still have two problems: 1) linking the tab to the pane 2) Assigning the class of "active" so the JavaScript is able to add and remove the CSS to reveal / hide the appropriate pane. ### 1) Linking to the Pane @@ -90,7 +90,7 @@ On the other hand, WordPress has a very useful function called Sanitize HTML, wh ### 2) Assigning the 'Active' Class -So now we need to get a class of 'active' _only on_ the first tab. The Bootstrap Javascript will do the rest for us. How do we do that? +So now we need to get a class of 'active' _only on_ the first tab. The Bootstrap JavaScript will do the rest for us. How do we do that? I added this code just inside the `while` loop, inside the `ul` tag: diff --git a/packages/astro/test/fixtures/server-islands/ssr/src/pages/test.mdx b/packages/astro/test/fixtures/server-islands/ssr/src/pages/test.mdx index 0d6cfb437ad2..430947205b8f 100644 --- a/packages/astro/test/fixtures/server-islands/ssr/src/pages/test.mdx +++ b/packages/astro/test/fixtures/server-islands/ssr/src/pages/test.mdx @@ -1,5 +1,5 @@ import Island from '../components/Island.astro'; -{/* empty div is needed, otherwise island script is injected in the head */} +{/* empty div is needed; otherwise, island script is injected in the head */}
diff --git a/packages/astro/test/fixtures/solid-component/src/components/async-components.jsx b/packages/astro/test/fixtures/solid-component/src/components/async-components.jsx index c292f1da2031..4fe6906327f1 100644 --- a/packages/astro/test/fixtures/solid-component/src/components/async-components.jsx +++ b/packages/astro/test/fixtures/solid-component/src/components/async-components.jsx @@ -1,6 +1,6 @@ import { ErrorBoundary, Show, createResource, createSignal, createUniqueId } from 'solid-js'; -// It may be good to try short and long sleep times. +// It may be good to try long and short sleep times. // But short is faster for testing. const SLEEP_MS = 10; diff --git a/packages/astro/test/fixtures/solid-component/src/pages/ssr-client-load.astro b/packages/astro/test/fixtures/solid-component/src/pages/ssr-client-load.astro index 0b43ca972373..2afb926ae450 100644 --- a/packages/astro/test/fixtures/solid-component/src/pages/ssr-client-load.astro +++ b/packages/astro/test/fixtures/solid-component/src/pages/ssr-client-load.astro @@ -10,7 +10,7 @@ import { AsyncComponent } from '../components/async-components.jsx'; - + diff --git a/packages/astro/test/fixtures/solid-component/src/pages/ssr-client-none.astro b/packages/astro/test/fixtures/solid-component/src/pages/ssr-client-none.astro index bf968b5aca2c..efae0c146dc6 100644 --- a/packages/astro/test/fixtures/solid-component/src/pages/ssr-client-none.astro +++ b/packages/astro/test/fixtures/solid-component/src/pages/ssr-client-none.astro @@ -9,7 +9,7 @@ import { AsyncComponent } from '../components/async-components.jsx';
- +
diff --git a/packages/astro/test/fixtures/ssr-request/src/middleware.ts b/packages/astro/test/fixtures/ssr-request/src/middleware.ts index 6e7a20d7a676..6f202913f4c3 100644 --- a/packages/astro/test/fixtures/ssr-request/src/middleware.ts +++ b/packages/astro/test/fixtures/ssr-request/src/middleware.ts @@ -1,8 +1,8 @@ import { defineMiddleware } from 'astro:middleware'; export const onRequest = defineMiddleware(({ url }, next) => { - // Redirect when there are extra slashes - if(url.pathname === '/this//is/my/////directory') { + // Redirect when the path matches (duplicate slashes are normalized before middleware runs) + if(url.pathname === '/this/is/my/directory') { return new Response(null, { status: 301, headers: { diff --git a/packages/astro/test/hmr-new-page.test.js b/packages/astro/test/hmr-new-page.test.js index 2065f6346d73..8f0b04a5be25 100644 --- a/packages/astro/test/hmr-new-page.test.js +++ b/packages/astro/test/hmr-new-page.test.js @@ -45,7 +45,7 @@ describe('HMR: New page detection', () => { assert.equal(response.status, 200); }); - it('should return 404 for non-existent page', async () => { + it('should return 404 for nonexistent page', async () => { const response = await fixture.fetch('/new-page'); assert.equal(response.status, 404); }); diff --git a/packages/astro/test/i18n-double-prefix.test.js b/packages/astro/test/i18n-double-prefix.test.js index 193932df844f..fafae8845344 100644 --- a/packages/astro/test/i18n-double-prefix.test.js +++ b/packages/astro/test/i18n-double-prefix.test.js @@ -37,7 +37,7 @@ describe('i18n double-prefix prevention', () => { }); it('should generate correct fallback redirects for missing Spanish pages', async () => { - // item2 only exists in English, so Spanish should fallback to English + // item2 only exists in English, so Spanish should fall back to English let spanishRedirect = false; try { await fixture.readFile('/es/test/item2/index.html'); diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.js index bf1d81797231..169e5c8a62a3 100644 --- a/packages/astro/test/rewrite.test.js +++ b/packages/astro/test/rewrite.test.js @@ -119,7 +119,7 @@ describe('Dev rewrite, trailing slash -> never, with base', () => { assert.equal($('p').text(), '/base'); }); - it('should rewrite and always inlcude base', async () => { + it('should rewrite and always include base', async () => { //rewrite('/') will rewrite to '/base' const html = await fixture.fetch('/base/bar').then((res) => res.text()); const $ = cheerioLoad(html); diff --git a/packages/astro/test/route-guard.test.js b/packages/astro/test/route-guard.test.js index dbc8cd902714..7f6303fb60d2 100644 --- a/packages/astro/test/route-guard.test.js +++ b/packages/astro/test/route-guard.test.js @@ -63,7 +63,7 @@ describe('Route Guard - Dev Server', () => { }); }); - describe('Non-existent files should 404 normally', () => { + describe('Nonexistent files should 404 normally', () => { it('404 when loading /nonexistent.md (file does not exist)', async () => { const response = await fixture.fetch('/nonexistent.md'); assert.equal(response.status, 404); diff --git a/packages/astro/test/ssr-request.test.js b/packages/astro/test/ssr-request.test.js index a520b350cceb..537592f3a7d0 100644 --- a/packages/astro/test/ssr-request.test.js +++ b/packages/astro/test/ssr-request.test.js @@ -96,7 +96,7 @@ describe('Using Astro.request in SSR', () => { assert.equal(data instanceof Array, true); }); - it('middleware gets the actual path sent in the request', async () => { + it('middleware gets the normalized path sent in the request', async () => { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com/this//is/my/////directory'); const response = await app.render(request); diff --git a/packages/astro/test/underscore-in-folder-name.test.js b/packages/astro/test/underscore-in-folder-name.test.js index b804a715cd8b..502f2b9310bc 100644 --- a/packages/astro/test/underscore-in-folder-name.test.js +++ b/packages/astro/test/underscore-in-folder-name.test.js @@ -3,7 +3,7 @@ import { before, describe, it } from 'node:test'; import testAdapter from './test-adapter.js'; import { loadFixture } from './test-utils.js'; -describe('Projects with a underscore in the folder name', () => { +describe('Projects with an underscore in the folder name', () => { let fixture; before(async () => { diff --git a/packages/astro/test/units/app/encoded-backslash-bypass.test.js b/packages/astro/test/units/app/encoded-backslash-bypass.test.js new file mode 100644 index 000000000000..fcf6c3e56d82 --- /dev/null +++ b/packages/astro/test/units/app/encoded-backslash-bypass.test.js @@ -0,0 +1,112 @@ +// @ts-check +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { App } from '../../../dist/core/app/app.js'; +import { parseRoute } from '../../../dist/core/routing/parse-route.js'; +import { createComponent, render } from '../../../dist/runtime/server/index.js'; +import { createManifest } from './test-helpers.js'; + +/** + * Tests that encoded backslash characters (%5C) in URL paths do not cause + * a mismatch between what middleware sees and what the router matches. + * + * When %5C is decoded to \, the URL spec's pathname setter normalizes \ to /, + * which can create unexpected double slashes in context.url.pathname. + * The middleware then sees a different path than what the router matched. + */ + +const routeOptions = /** @type {Parameters[1]} */ ( + /** @type {any} */ ({ + config: { base: '/', trailingSlash: 'ignore' }, + pageExtensions: [], + }) +); + +// Dynamic route: /users/[slug] +const userSlugRouteData = parseRoute('users/[slug]', routeOptions, { + component: 'src/pages/users/[slug].astro', +}); + +const publicRouteData = parseRoute('index.astro', routeOptions, { + component: 'src/pages/index.astro', +}); + +const page = createComponent(() => { + return render`

Page

`; +}); + +const pageModule = async () => ({ + page: async () => ({ + default: page, + }), +}); + +const pageMap = new Map([ + [userSlugRouteData.component, pageModule], + [publicRouteData.component, pageModule], +]); + +/** + * Middleware that blocks access to /users/admin path, + * simulating authorization checks on dynamic routes. + */ +const middleware = + /** @type {() => Promise<{onRequest: import('../../../dist/types/public/common.js').MiddlewareHandler}>} */ ( + async () => ({ + onRequest: async (context, next) => { + const pathname = context.url.pathname; + if (pathname === '/users/admin' || pathname.startsWith('/users/admin/')) { + return new Response('Forbidden', { status: 403 }); + } + return next(); + }, + }) + ); + +const app = new App( + createManifest({ + routes: [{ routeData: userSlugRouteData }, { routeData: publicRouteData }], + pageMap, + middleware, + }), +); + +describe('URL normalization: encoded backslash handling in pathname', () => { + it('middleware blocks /users/admin with normal request', async () => { + const request = new Request('http://example.com/users/admin'); + const response = await app.render(request); + assert.equal(response.status, 403, '/users/admin should be blocked by middleware'); + }); + + it('middleware blocks /users/%5Cadmin (encoded backslash)', async () => { + const request = new Request('http://example.com/users/%5Cadmin'); + const response = await app.render(request); + // After decoding %5C to \, and URL normalization of \ to /, + // this should become /users/admin which the middleware should block + assert.equal(response.status, 403, '/users/%5Cadmin should be blocked by middleware'); + }); + + it('middleware blocks /users/%5cadmin (lowercase hex encoded backslash)', async () => { + const request = new Request('http://example.com/users/%5cadmin'); + const response = await app.render(request); + assert.equal(response.status, 403, '/users/%5cadmin should be blocked by middleware'); + }); + + it('middleware blocks /users/%5C%5Cadmin (double encoded backslash)', async () => { + const request = new Request('http://example.com/users/%5C%5Cadmin'); + const response = await app.render(request); + assert.equal(response.status, 403, '/users/%5C%5Cadmin should be blocked by middleware'); + }); + + it('public route is still accessible', async () => { + const request = new Request('http://example.com/'); + const response = await app.render(request); + assert.equal(response.status, 200, '/ should be accessible'); + }); + + it('non-protected dynamic route is still accessible', async () => { + const request = new Request('http://example.com/users/john'); + const response = await app.render(request); + assert.equal(response.status, 200, '/users/john should be accessible'); + }); +}); diff --git a/packages/astro/test/units/app/node.test.js b/packages/astro/test/units/app/node.test.js index 1f3305e4dc1f..e820cd8e84a7 100644 --- a/packages/astro/test/units/app/node.test.js +++ b/packages/astro/test/units/app/node.test.js @@ -243,7 +243,7 @@ describe('node', () => { assert.equal(result.url, 'https://example.com/'); }); - it('bad values are ignored and fallback to host header', () => { + it('bad values are ignored and fall back to host header', () => { const result = createRequest( { ...mockNodeRequest, @@ -282,7 +282,7 @@ describe('node', () => { }, { allowedDomains: [{ hostname: '*.victim.com' }] }, ); - // localhost should not match *.victim.com, fallback to host header which does match + // localhost should not match *.victim.com, fall back to host header which does match assert.equal(result.url, 'https://sub.victim.com/'); }); @@ -855,6 +855,107 @@ describe('node', () => { }); }); + describe('body size limit', () => { + it('rejects request body that exceeds the configured bodySizeLimit', async () => { + const { Readable } = await import('node:stream'); + // Create a stream that produces data exceeding the limit + const limit = 1024; // 1KB limit + const chunks = []; + // Create 2KB of data (exceeds 1KB limit) + for (let i = 0; i < 4; i++) { + chunks.push(Buffer.alloc(512, 0x41)); + } + const stream = Readable.from(chunks); + const req = { + ...mockNodeRequest, + method: 'POST', + headers: { + ...mockNodeRequest.headers, + 'content-type': 'application/octet-stream', + }, + socket: mockNodeRequest.socket, + [Symbol.asyncIterator]: stream[Symbol.asyncIterator].bind(stream), + }; + + const request = createRequest(req, { bodySizeLimit: limit }); + + // The request should be created, but reading the body should fail + await assert.rejects( + async () => { + const reader = request.body.getReader(); + while (true) { + const { done } = await reader.read(); + if (done) break; + } + }, + (err) => { + assert.ok(err.message.includes('Body size limit exceeded')); + return true; + }, + ); + }); + + it('allows request body within the configured bodySizeLimit', async () => { + const { Readable } = await import('node:stream'); + const limit = 2048; // 2KB limit + const data = Buffer.alloc(1024, 0x42); // 1KB of data (within limit) + const stream = Readable.from([data]); + const req = { + ...mockNodeRequest, + method: 'POST', + headers: { + ...mockNodeRequest.headers, + 'content-type': 'application/octet-stream', + }, + socket: mockNodeRequest.socket, + [Symbol.asyncIterator]: stream[Symbol.asyncIterator].bind(stream), + }; + + const request = createRequest(req, { bodySizeLimit: limit }); + + // Reading the body should succeed + const reader = request.body.getReader(); + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + const totalSize = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + assert.equal(totalSize, 1024); + }); + + it('does not enforce body size limit when bodySizeLimit is not set', async () => { + const { Readable } = await import('node:stream'); + // Create 2KB of data with no limit configured + const data = Buffer.alloc(2048, 0x43); + const stream = Readable.from([data]); + const req = { + ...mockNodeRequest, + method: 'POST', + headers: { + ...mockNodeRequest.headers, + 'content-type': 'application/octet-stream', + }, + socket: mockNodeRequest.socket, + [Symbol.asyncIterator]: stream[Symbol.asyncIterator].bind(stream), + }; + + const request = createRequest(req); + + // Reading the body should succeed without limit + const reader = request.body.getReader(); + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + const totalSize = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + assert.equal(totalSize, 2048); + }); + }); + describe('abort signal', () => { it('aborts the request.signal when the underlying socket closes', () => { const socket = new EventEmitter(); diff --git a/packages/astro/test/units/assets/fonts/core.test.js b/packages/astro/test/units/assets/fonts/core.test.js index 3c91e392e8fd..2ce75a1aceac 100644 --- a/packages/astro/test/units/assets/fonts/core.test.js +++ b/packages/astro/test/units/assets/fonts/core.test.js @@ -194,7 +194,7 @@ describe('fonts core', () => { }, filterAndTransformFontFaces: () => [ { - src: [{ url: 'overriden' }], + src: [{ url: 'overridden' }], }, ], collectFontAssetsFromFaces: () => { @@ -211,7 +211,7 @@ describe('fonts core', () => { family: families[0], fonts: [ { - src: [{ url: 'overriden' }], + src: [{ url: 'overridden' }], }, ], preloads: [], diff --git a/packages/astro/test/units/config/config-tsconfig.test.js b/packages/astro/test/units/config/config-tsconfig.test.js index a82ef8b25d7f..94e9438982fd 100644 --- a/packages/astro/test/units/config/config-tsconfig.test.js +++ b/packages/astro/test/units/config/config-tsconfig.test.js @@ -24,7 +24,7 @@ describe('TSConfig handling', () => { assert.deepEqual(config.tsconfig.files, ['im-a-test']); }); - it('can fallback to jsconfig.json if tsconfig.json does not exists', async () => { + it('can fall back to jsconfig.json if tsconfig.json does not exist', async () => { const config = await loadTSConfig(path.join(cwd, 'jsconfig')); assert.equal(config !== undefined, true); diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js index d3755dcf5f68..8938c14e166d 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.js @@ -226,7 +226,7 @@ describe('Config Validation', () => { assert.equal(configError instanceof z.ZodError, true); assert.equal( configError.issues[0].message, - 'The option `i18n.routing.redirectToDefaultLocale` can be used only when `i18n.routing.prefixDefaultLocale` is set to `true`, otherwise redirects might cause infinite loops. Remove the option `i18n.routing.redirectToDefaultLocale`, or change its value to `false`.', + 'The option `i18n.routing.redirectToDefaultLocale` can be used only when `i18n.routing.prefixDefaultLocale` is set to `true`; otherwise, redirects might cause infinite loops. Remove the option `i18n.routing.redirectToDefaultLocale`, or change its value to `false`.', ); }, ); diff --git a/packages/astro/test/units/dev/dev.test.js b/packages/astro/test/units/dev/dev.test.js index 74f1d1e92e0d..8867976feba8 100644 --- a/packages/astro/test/units/dev/dev.test.js +++ b/packages/astro/test/units/dev/dev.test.js @@ -125,7 +125,7 @@ describe('dev container', () => { assert.equal(r.res.statusCode, 404); } { - // A non-existent page also serves the custom 404 page. + // A nonexistent page also serves the custom 404 page. const r = createRequestAndResponse({ method: 'GET', url: '/other-page' }); container.handle(r.req, r.res); await r.done; diff --git a/packages/astro/test/units/env/env-validators.test.js b/packages/astro/test/units/env/env-validators.test.js index 02c2b5a56c4e..1668408a9726 100644 --- a/packages/astro/test/units/env/env-validators.test.js +++ b/packages/astro/test/units/env/env-validators.test.js @@ -1,6 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { getEnvFieldType, validateEnvVariable } from '../../../dist/env/validators.js'; +import { + getEnvFieldType, + validateEnvVariable, + validateEnvPrefixAgainstSchema, +} from '../../../dist/env/validators.js'; /** * @typedef {Parameters} Params @@ -550,3 +554,129 @@ describe('astro:env validators', () => { }); }); }); + +describe('validateEnvPrefixAgainstSchema', () => { + /** + * Helper to build a minimal config object matching the shape + * validateEnvPrefixAgainstSchema expects. + * + * @param {Record} schema + * @param {string | string[] | undefined} envPrefix + */ + function makeConfig(schema, envPrefix) { + return /** @type {any} */ ({ + env: { schema }, + vite: envPrefix !== undefined ? { envPrefix } : {}, + }); + } + + it('should not throw when schema is empty', () => { + assert.doesNotThrow(() => { + validateEnvPrefixAgainstSchema(makeConfig({}, ['PUBLIC_', 'API_'])); + }); + }); + + it('should not throw when envPrefix is not set', () => { + assert.doesNotThrow(() => { + validateEnvPrefixAgainstSchema( + makeConfig( + { API_SECRET: { context: 'server', access: 'secret', type: 'string' } }, + undefined, + ), + ); + }); + }); + + it('should not throw when envPrefix does not match any secret variable', () => { + assert.doesNotThrow(() => { + validateEnvPrefixAgainstSchema( + makeConfig({ DB_PASSWORD: { context: 'server', access: 'secret', type: 'string' } }, [ + 'PUBLIC_', + 'API_', + ]), + ); + }); + }); + + it('should not throw when envPrefix matches a public variable', () => { + assert.doesNotThrow(() => { + validateEnvPrefixAgainstSchema( + makeConfig({ API_URL: { context: 'server', access: 'public', type: 'string' } }, [ + 'PUBLIC_', + 'API_', + ]), + ); + }); + }); + + it('should throw when envPrefix matches a secret variable (array prefix)', () => { + assert.throws( + () => { + validateEnvPrefixAgainstSchema( + makeConfig({ API_SECRET: { context: 'server', access: 'secret', type: 'string' } }, [ + 'PUBLIC_', + 'API_', + ]), + ); + }, + (err) => { + assert.equal(err.name, 'EnvPrefixConflictsWithSecret'); + assert.equal(err.message.includes('API_SECRET'), true); + return true; + }, + ); + }); + + it('should throw when envPrefix matches a secret variable (string prefix)', () => { + assert.throws( + () => { + validateEnvPrefixAgainstSchema( + makeConfig( + { SECRET_KEY: { context: 'server', access: 'secret', type: 'string' } }, + 'SECRET_', + ), + ); + }, + (err) => { + assert.equal(err.name, 'EnvPrefixConflictsWithSecret'); + assert.equal(err.message.includes('SECRET_KEY'), true); + return true; + }, + ); + }); + + it('should list all conflicting secret variables in the error', () => { + assert.throws( + () => { + validateEnvPrefixAgainstSchema( + makeConfig( + { + API_SECRET: { context: 'server', access: 'secret', type: 'string' }, + API_KEY: { context: 'server', access: 'secret', type: 'string' }, + API_URL: { context: 'server', access: 'public', type: 'string' }, + }, + ['PUBLIC_', 'API_'], + ), + ); + }, + (err) => { + assert.equal(err.name, 'EnvPrefixConflictsWithSecret'); + assert.equal(err.message.includes('API_SECRET'), true); + assert.equal(err.message.includes('API_KEY'), true); + assert.equal(err.message.includes('API_URL'), false); + return true; + }, + ); + }); + + it('should not throw when only the default PUBLIC_ prefix is used', () => { + assert.doesNotThrow(() => { + validateEnvPrefixAgainstSchema( + makeConfig( + { DB_PASSWORD: { context: 'server', access: 'secret', type: 'string' } }, + 'PUBLIC_', + ), + ); + }); + }); +}); diff --git a/packages/astro/test/units/middleware/middleware-app.test.js b/packages/astro/test/units/middleware/middleware-app.test.js index 284edbc88281..f8de122ab568 100644 --- a/packages/astro/test/units/middleware/middleware-app.test.js +++ b/packages/astro/test/units/middleware/middleware-app.test.js @@ -761,6 +761,132 @@ describe('Middleware via App.render()', () => { }); }); + describe('framing headers on error pages', () => { + it('should not preserve Content-Length from middleware when rendering 404 error page', async () => { + // Middleware calls next(), then decides to return 404 with a stale Content-Length header. + // On re-render for the error page, middleware passes the response through unchanged. + let callCount = 0; + const onRequest = async (ctx, next) => { + callCount++; + const response = await next(); + if (callCount === 1 && ctx.url.pathname.startsWith('/api/guarded')) { + return new Response(null, { + status: 404, + headers: { 'Content-Length': '999', 'X-Custom': 'keep-me' }, + }); + } + return response; + }; + + const guardedRouteData = createRouteData({ + route: '/api/guarded/[...path]', + pathname: undefined, + segments: undefined, + }); + guardedRouteData.params = ['...path']; + guardedRouteData.pattern = /^\/api\/guarded(?:\/(.*))?$/; + guardedRouteData.pathname = undefined; + guardedRouteData.segments = [ + [{ content: 'api', dynamic: false, spread: false }], + [{ content: 'guarded', dynamic: false, spread: false }], + [{ content: '...path', dynamic: true, spread: true }], + ]; + + const pageMap = new Map([ + [ + guardedRouteData.component, + async () => ({ + page: async () => ({ + default: simplePage(), + }), + }), + ], + [ + notFoundRouteData.component, + async () => ({ page: async () => ({ default: notFoundPage }) }), + ], + ]); + const app = createAppWithMiddleware({ + onRequest, + routes: [{ routeData: guardedRouteData }, { routeData: notFoundRouteData }], + pageMap, + }); + + const response = await app.render(new Request('http://localhost/api/guarded/secret')); + + assert.equal(response.status, 404); + // Content-Length from middleware's original response must not leak into the error page response + assert.equal( + response.headers.get('Content-Length'), + null, + 'Content-Length from middleware should be stripped during error page merge', + ); + // Non-framing custom headers should still be preserved + assert.equal(response.headers.get('X-Custom'), 'keep-me'); + }); + + it('should not preserve Transfer-Encoding from middleware when rendering 500 error page', async () => { + let callCount = 0; + const onRequest = async (ctx, next) => { + callCount++; + const response = await next(); + if (callCount === 1 && ctx.url.pathname.startsWith('/api/error')) { + return new Response(null, { + status: 500, + headers: { 'Transfer-Encoding': 'chunked', 'X-Error-Source': 'middleware' }, + }); + } + return response; + }; + + const errorRouteData = createRouteData({ + route: '/api/error/[...path]', + pathname: undefined, + segments: undefined, + }); + errorRouteData.params = ['...path']; + errorRouteData.pattern = /^\/api\/error(?:\/(.*))?$/; + errorRouteData.pathname = undefined; + errorRouteData.segments = [ + [{ content: 'api', dynamic: false, spread: false }], + [{ content: 'error', dynamic: false, spread: false }], + [{ content: '...path', dynamic: true, spread: true }], + ]; + + const pageMap = new Map([ + [ + errorRouteData.component, + async () => ({ + page: async () => ({ + default: simplePage(), + }), + }), + ], + [ + serverErrorRouteData.component, + async () => ({ page: async () => ({ default: serverErrorPage }) }), + ], + ]); + const app = createAppWithMiddleware({ + onRequest, + routes: [{ routeData: errorRouteData }, { routeData: serverErrorRouteData }], + pageMap, + }); + + const response = await app.render(new Request('http://localhost/api/error/test')); + + assert.equal(response.status, 500); + // Transfer-Encoding from middleware's original response must not leak into the error page response + assert.equal( + response.headers.get('Transfer-Encoding'), + null, + 'Transfer-Encoding from middleware should be stripped during error page merge', + ); + // Non-framing custom headers should still be preserved + assert.equal(response.headers.get('X-Error-Source'), 'middleware'); + }); + }); + describe('middleware with custom headers', () => { it('should correctly set custom headers in middleware', async () => { const onRequest = async (_ctx, next) => { diff --git a/packages/astro/test/units/render/render-context.test.js b/packages/astro/test/units/render/render-context.test.js new file mode 100644 index 000000000000..aa4068cda721 --- /dev/null +++ b/packages/astro/test/units/render/render-context.test.js @@ -0,0 +1,130 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { RenderContext } from '../../../dist/core/render-context.js'; +import { createComponent, maybeRenderHead, render } from '../../../dist/runtime/server/index.js'; +import { createBasicPipeline } from '../test-utils.js'; + +const createAstroModule = (AstroComponent) => ({ default: AstroComponent }); + +describe('RenderContext', () => { + describe('skipMiddleware and form action handling', () => { + it('does not auto-execute form actions when skipMiddleware is true', async () => { + let actionWasCalled = false; + + const pipeline = createBasicPipeline({ + manifest: { + rootDir: import.meta.url, + serverLike: true, + experimentalQueuedRendering: { enabled: true }, + }, + }); + + // Set up a mock action on the pipeline + pipeline.resolvedActions = { + server: { + testAction: async function () { + actionWasCalled = true; + return { data: 'should not be called', error: undefined }; + }, + }, + }; + + const SimplePage = createComponent((result) => { + return render`${maybeRenderHead(result)}

Error page

`; + }); + const PageModule = createAstroModule(SimplePage); + + // POST request with _action param (simulates form action submission) + const request = new Request('http://example.com/404?_action=testAction', { + method: 'POST', + body: new FormData(), + }); + + const routeData = { + type: 'page', + pathname: '/404', + component: 'src/pages/404.astro', + params: {}, + route: '/404', + prerender: false, + }; + + // Create context with skipMiddleware=true (as happens during error recovery) + const renderContext = await RenderContext.create({ + pipeline, + request, + routeData, + status: 404, + skipMiddleware: true, + }); + + const response = await renderContext.render(PageModule); + + assert.equal(response.status, 404); + assert.equal( + actionWasCalled, + false, + 'Form action should not be auto-executed when skipMiddleware is true', + ); + }); + + it('auto-executes form actions when skipMiddleware is false', async () => { + let actionWasCalled = false; + + const pipeline = createBasicPipeline({ + manifest: { + rootDir: import.meta.url, + serverLike: true, + experimentalQueuedRendering: { enabled: true }, + }, + }); + + // Set up a mock action on the pipeline + pipeline.resolvedActions = { + server: { + testAction: async function () { + actionWasCalled = true; + return { data: 'action result', error: undefined }; + }, + }, + }; + + const SimplePage = createComponent((result) => { + return render`${maybeRenderHead(result)}

Page

`; + }); + const PageModule = createAstroModule(SimplePage); + + // POST request with _action param (simulates form action submission) + const request = new Request('http://example.com/page?_action=testAction', { + method: 'POST', + body: new FormData(), + }); + + const routeData = { + type: 'page', + pathname: '/page', + component: 'src/pages/page.astro', + params: {}, + route: '/page', + prerender: false, + }; + + // Create context with skipMiddleware=false (normal flow) + const renderContext = await RenderContext.create({ + pipeline, + request, + routeData, + skipMiddleware: false, + }); + + const response = await renderContext.render(PageModule); + + assert.equal(response.status, 200); + assert.equal( + actionWasCalled, + true, + 'Form action should be auto-executed when skipMiddleware is false', + ); + }); + }); +}); diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index de096e6f29e4..d26a6dec3f12 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -373,7 +373,7 @@ describe('routing - createRoutesList', () => { { label: 'router', level: 'warn', - message: 'A collision will result in an hard error in following versions of Astro.', + message: 'A collision will result in a hard error in following versions of Astro.', newLine: true, }, ]); @@ -412,7 +412,7 @@ describe('routing - createRoutesList', () => { { label: 'router', level: 'warn', - message: 'A collision will result in an hard error in following versions of Astro.', + message: 'A collision will result in a hard error in following versions of Astro.', newLine: true, }, ]); diff --git a/packages/astro/test/units/vite-plugin-astro/compile.test.js b/packages/astro/test/units/vite-plugin-astro/compile.test.js index b25fcdf90d51..6f0daed3b5ef 100644 --- a/packages/astro/test/units/vite-plugin-astro/compile.test.js +++ b/packages/astro/test/units/vite-plugin-astro/compile.test.js @@ -59,7 +59,7 @@ const name = 'world assert.equal(result, undefined); }); - it('has file and url exports for markdwon compat', async () => { + it('has file and url exports for markdown compat', async () => { const result = await compile(`

Hello World

`, '/src/components/index.astro'); await init; const [, exports] = parse(result.code); diff --git a/packages/create-astro/CHANGELOG.md b/packages/create-astro/CHANGELOG.md index b34dccee514b..5863374dd3dd 100644 --- a/packages/create-astro/CHANGELOG.md +++ b/packages/create-astro/CHANGELOG.md @@ -164,7 +164,7 @@ ### Minor Changes -- [#11924](https://github.com/withastro/astro/pull/11924) [`7d70ba3`](https://github.com/withastro/astro/commit/7d70ba317889b9281c7891038779a68fcb8f0778) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Updates the default Astro config with `// @ts-check` if the Typescript preset is `strict` or `strictest` +- [#11924](https://github.com/withastro/astro/pull/11924) [`7d70ba3`](https://github.com/withastro/astro/commit/7d70ba317889b9281c7891038779a68fcb8f0778) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Updates the default Astro config with `// @ts-check` if the TypeScript preset is `strict` or `strictest` ## 4.8.4 @@ -398,7 +398,7 @@ ### Patch Changes -- [#7527](https://github.com/withastro/astro/pull/7527) [`9e2426f75`](https://github.com/withastro/astro/commit/9e2426f75637a6318961f483de90b635f3fdadeb) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Default registry logic to fallback to NPM if registry command fails (sorry, Bun users!) +- [#7527](https://github.com/withastro/astro/pull/7527) [`9e2426f75`](https://github.com/withastro/astro/commit/9e2426f75637a6318961f483de90b635f3fdadeb) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Default registry logic to fall back to NPM if registry command fails (sorry, Bun users!) - [#7539](https://github.com/withastro/astro/pull/7539) [`1170877b5`](https://github.com/withastro/astro/commit/1170877b51aaa13203e8c488dcf4e39d1b5553ee) Thanks [@jc1144096387](https://github.com/jc1144096387)! - Update registry logic, improving edge cases (http support, redirects, registries ending with '/') @@ -498,7 +498,7 @@ ### Patch Changes -- [#5953](https://github.com/withastro/astro/pull/5953) [`5c64324c0`](https://github.com/withastro/astro/commit/5c64324c0a1b06e836c3d53668940faca4cb517d) Thanks [@ZermattChris](https://github.com/ZermattChris)! - Check for a pre-existing .git directory and if found, skip trying to create a new one. +- [#5953](https://github.com/withastro/astro/pull/5953) [`5c64324c0`](https://github.com/withastro/astro/commit/5c64324c0a1b06e836c3d53668940faca4cb517d) Thanks [@ZermattChris](https://github.com/ZermattChris)! - Check for a preexisting .git directory and if found, skip trying to create a new one. ## 2.0.1 diff --git a/packages/create-astro/create-astro.mjs b/packages/create-astro/create-astro.mjs index 271abafcd20f..f72274e157fa 100755 --- a/packages/create-astro/create-astro.mjs +++ b/packages/create-astro/create-astro.mjs @@ -9,7 +9,7 @@ const IS_STACKBLITZ = !!process.versions.webcontainer; const minimumMajorVersion = IS_STACKBLITZ ? 20 : 22; if (requiredMajorVersion < minimumMajorVersion) { - console.error(`Node.js v${currentVersion} is out of date and unsupported!`); + console.error(`Node.js v${currentVersion} is out-of-date and unsupported!`); console.error(`Please use Node.js v${minimumMajorVersion} or higher.`); process.exit(1); } diff --git a/packages/create-astro/src/actions/context.ts b/packages/create-astro/src/actions/context.ts index 44a6fbd27e7b..7848d478f010 100644 --- a/packages/create-astro/src/actions/context.ts +++ b/packages/create-astro/src/actions/context.ts @@ -39,7 +39,7 @@ function getPackageTag(packageSpecifier: string | undefined): string | undefined case 'beta': case 'rc': return packageSpecifier; - // Will fallback to latest + // Will fall back to latest case undefined: default: return undefined; diff --git a/packages/create-astro/src/actions/template.ts b/packages/create-astro/src/actions/template.ts index 058cb2c33954..3cd447cea3cd 100644 --- a/packages/create-astro/src/actions/template.ts +++ b/packages/create-astro/src/actions/template.ts @@ -96,7 +96,7 @@ const FILES_TO_REMOVE = ['CHANGELOG.md', '.codesandbox']; const FILES_TO_UPDATE = { 'package.json': (file: string, overrides: { name: string }) => fs.promises.readFile(file, 'utf-8').then((value) => { - // Match first indent in the file or fallback to `\t` + // Match first indent in the file or fall back to `\t` const indent = /(^\s+)/m.exec(value)?.[1] ?? '\t'; return fs.promises.writeFile( file, @@ -124,7 +124,7 @@ export function getTemplateTarget(tmpl: string, ref = 'latest') { // Handle Astro templates if (ref === 'latest') { // `latest` ref is specially handled to route to a branch specifically - // to allow faster downloads. Otherwise giget has to download the entire + // to allow faster downloads. Otherwise, giget has to download the entire // repo and only copy a sub directory return `github:withastro/astro#examples/${tmpl}`; } else { diff --git a/packages/create-astro/test/template-processing.test.js b/packages/create-astro/test/template-processing.test.js index d85b4fc7e6ab..a14a979e7e34 100644 --- a/packages/create-astro/test/template-processing.test.js +++ b/packages/create-astro/test/template-processing.test.js @@ -67,7 +67,7 @@ Keep this.`; assert.equal(result.trim(), expected.trim()); }); - it('is case insensitive', async () => { + it('is case-insensitive', async () => { const content = `# Title diff --git a/packages/db/CHANGELOG.md b/packages/db/CHANGELOG.md index ebb7b3ea2fc5..099326dfa548 100644 --- a/packages/db/CHANGELOG.md +++ b/packages/db/CHANGELOG.md @@ -402,7 +402,7 @@ - [#11304](https://github.com/withastro/astro/pull/11304) [`2e70741`](https://github.com/withastro/astro/commit/2e70741362afc1e7d03c8b2a9d8edb8466dfe9c3) Thanks [@Fryuni](https://github.com/Fryuni)! - Removes the `AstroDbIntegration` type - Astro integration hooks can now be extended and as such `@astrojs/db` no longer needs to declare it's own integration type. Using `AstroIntegration` will have the same type. + Astro integration hooks can now be extended and as such `@astrojs/db` no longer needs to declare its own integration type. Using `AstroIntegration` will have the same type. If you were using the `AstroDbIntegration` type, apply this change to your integration code: diff --git a/packages/db/test/basics.test.js b/packages/db/test/basics.test.js index c2d6aaea0df9..6186af174703 100644 --- a/packages/db/test/basics.test.js +++ b/packages/db/test/basics.test.js @@ -69,7 +69,7 @@ describe('astro:db', () => { assert.match(themeDark, /dark mode/); }); - it('text fields an be used as references', async () => { + it('text fields can be used as references', async () => { const html = await fixture.fetch('/login').then((res) => res.text()); const $ = cheerioLoad(html); @@ -151,7 +151,7 @@ describe('astro:db', () => { assert.match(themeDark, /dark mode/); }); - it('text fields an be used as references', async () => { + it('text fields can be used as references', async () => { const html = await fixture.fetch('/login').then((res) => res.text()); const $ = cheerioLoad(html); diff --git a/packages/integrations/cloudflare/CHANGELOG.md b/packages/integrations/cloudflare/CHANGELOG.md index 1458e7363684..b3095dd8fe0f 100644 --- a/packages/integrations/cloudflare/CHANGELOG.md +++ b/packages/integrations/cloudflare/CHANGELOG.md @@ -161,7 +161,7 @@ ### Patch Changes -- [#15432](https://github.com/withastro/astro/pull/15432) [`e2ad69e`](https://github.com/withastro/astro/commit/e2ad69ebdef907c5365bd1047772a3e86736c9d2) Thanks [@OliverSpeir](https://github.com/OliverSpeir)! - Removes unneccessary warning about sharp from being printed at start of dev server and build +- [#15432](https://github.com/withastro/astro/pull/15432) [`e2ad69e`](https://github.com/withastro/astro/commit/e2ad69ebdef907c5365bd1047772a3e86736c9d2) Thanks [@OliverSpeir](https://github.com/OliverSpeir)! - Removes unnecessary warning about sharp from being printed at start of dev server and build - Updated dependencies [[`a164c77`](https://github.com/withastro/astro/commit/a164c77336059f2dc3e7f7fe992aa754ed145ef3), [`a18d727`](https://github.com/withastro/astro/commit/a18d727fc717054df85177c8e0c3d38a5252f2da)]: - @astrojs/internal-helpers@0.8.0-beta.1 @@ -229,7 +229,7 @@ - [#15080](https://github.com/withastro/astro/pull/15080) [`f67b738`](https://github.com/withastro/astro/commit/f67b7389dad9a3a258ba5d941d478f20b0dc6767) Thanks [@gameroman](https://github.com/gameroman)! - Updates `wrangler` dependency to be a `peerDependency` over a `dependency` -- [#15121](https://github.com/withastro/astro/pull/15121) [`06261e0`](https://github.com/withastro/astro/commit/06261e03d55a571c6affbd7321f7e28c997d6d5d) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a bug where the Astro, with the Cloudlfare integration, couldn't correctly serve certain routes in the development server. +- [#15121](https://github.com/withastro/astro/pull/15121) [`06261e0`](https://github.com/withastro/astro/commit/06261e03d55a571c6affbd7321f7e28c997d6d5d) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a bug where the Astro, with the Cloudflare integration, couldn't correctly serve certain routes in the development server. - Updated dependencies []: - @astrojs/underscore-redirects@1.0.0 @@ -561,7 +561,7 @@ - [#14066](https://github.com/withastro/astro/pull/14066) [`7abde79`](https://github.com/withastro/astro/commit/7abde7921fb21058d99180d6a0c897c5fa23ff14) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Refactors the internal solution which powers Astro Sessions when running local development with ˋastro devˋ. - The adapter now utilizes Cloudflare's local support for Cloudflare KV. This internal change is a drop-in replacement and does not require any change to your projectct code. + The adapter now utilizes Cloudflare's local support for Cloudflare KV. This internal change is a drop-in replacement and does not require any change to your project code. However, you now have the ability to connect to the remote Cloudflare KV Namespace if desired and use production data during local development. @@ -940,7 +940,7 @@ ### Patch Changes -- [#476](https://github.com/withastro/adapters/pull/476) [`a8a8ab1`](https://github.com/withastro/adapters/commit/a8a8ab12d9cfb5157e6a350b93a505010367b8e4) Thanks [@bluwy](https://github.com/bluwy)! - Removes resolving with "node" conditionto fix Vue imports +- [#476](https://github.com/withastro/adapters/pull/476) [`a8a8ab1`](https://github.com/withastro/adapters/commit/a8a8ab12d9cfb5157e6a350b93a505010367b8e4) Thanks [@bluwy](https://github.com/bluwy)! - Removes resolving with "node" condition to fix Vue imports ## 12.0.1 @@ -1088,7 +1088,7 @@ ### Patch Changes -- [#341](https://github.com/withastro/adapters/pull/341) [`a430ab1`](https://github.com/withastro/adapters/commit/a430ab17e525492db2ff9ecc4d00eb710dd92874) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Fixes an issue if environment variables where used inside the middleware and a prerendering occured. +- [#341](https://github.com/withastro/adapters/pull/341) [`a430ab1`](https://github.com/withastro/adapters/commit/a430ab17e525492db2ff9ecc4d00eb710dd92874) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Fixes an issue if environment variables where used inside the middleware and a prerendering occurred. - [#335](https://github.com/withastro/adapters/pull/335) [`237f332`](https://github.com/withastro/adapters/commit/237f332a819a92cdc2128d1564f5b8558318ad2b) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Fixes an issue displaying images which are optimized during `astro build` @@ -1096,7 +1096,7 @@ ### Patch Changes -- [#340](https://github.com/withastro/adapters/pull/340) [`45d0abb`](https://github.com/withastro/adapters/commit/45d0abb52b8e940a7c702a148be779428836396c) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Fixes an issue if environment variables where used inside the middleware and a prerendering occured. +- [#340](https://github.com/withastro/adapters/pull/340) [`45d0abb`](https://github.com/withastro/adapters/commit/45d0abb52b8e940a7c702a148be779428836396c) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Fixes an issue if environment variables where used inside the middleware and a prerendering occurred. ## 11.0.1 @@ -1217,7 +1217,7 @@ ### BREAKING: `imageService` - This release changes the default behavior of `imageService`. In the past the default behavior was falling back to a `noop` service, which disabled image optimization for your project, because Cloudflare doesn's support it. The new default is `compile`, which enables image optimization for prerendered pages during build, but disallows the usage of any `astro:assets` feature inside of on-demand pages. + This release changes the default behavior of `imageService`. In the past, the default behavior was falling back to a `noop` service, which disabled image optimization for your project, because Cloudflare doesn's support it. The new default is `compile`, which enables image optimization for prerendered pages during build, but disallows the usage of any `astro:assets` feature inside of on-demand pages. #### What should I do? @@ -1379,13 +1379,13 @@ ### Major Changes -- [#159](https://github.com/withastro/adapters/pull/159) [`adb8bf2a4caeead9a1a255740c7abe8666a6f852`](https://github.com/withastro/adapters/commit/adb8bf2a4caeead9a1a255740c7abe8666a6f852) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Updates and prepares the adapter to be more flexibile, stable and composable for the future. Includes several breaking changes. +- [#159](https://github.com/withastro/adapters/pull/159) [`adb8bf2a4caeead9a1a255740c7abe8666a6f852`](https://github.com/withastro/adapters/commit/adb8bf2a4caeead9a1a255740c7abe8666a6f852) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Updates and prepares the adapter to be more flexible, stable and composable for the future. Includes several breaking changes. ## Upgrade Guide - We are commited to provide a smooth upgrade path for our users. This guide will describe what has changed from v9.x to v10 to help you to migrate your existing projects to the latest version of the adapter. For complete documentation of all v10 configuration settings and usage, please see [the current, updated Cloudflare adapter documentation](https://docs.astro.build/en/guides/integrations-guide/cloudflare/). + We are committed to provide a smooth upgrade path for our users. This guide will describe what has changed from v9.x to v10 to help you to migrate your existing projects to the latest version of the adapter. For complete documentation of all v10 configuration settings and usage, please see [the current, updated Cloudflare adapter documentation](https://docs.astro.build/en/guides/integrations-guide/cloudflare/). - We will provide at least 4 weeks of limited maintanance support for the previous version 9 of the adapter. Please plan to upgrade your project within this time frame, using the instructions below. + We will provide at least 4 weeks of limited maintenance support for the previous version 9 of the adapter. Please plan to upgrade your project within this time frame, using the instructions below. ### Adapter's `mode` option & Cloudflare Functions @@ -1505,7 +1505,7 @@ ### Routes - The `routes.strategy` option has been removed as you will no longer have the option to choose a strategy in v10 of this adpater. + The `routes.strategy` option has been removed as you will no longer have the option to choose a strategy in v10 of this adapter. If you are using `routes.strategy`, you can remove it. You might observe a different `dist/_routes.json` file, but it should not affect your project's behavior. @@ -1729,7 +1729,7 @@ ### Patch Changes -- [#8782](https://github.com/withastro/astro/pull/8782) [`75781643a`](https://github.com/withastro/astro/commit/75781643a2f53656fc3fde3a7f28cb62db40b015) Thanks [@helloimalastair](https://github.com/helloimalastair)! - fixes `AdvancedRuntime` & `DirectoryRuntime` types to work woth Cloudflare caches +- [#8782](https://github.com/withastro/astro/pull/8782) [`75781643a`](https://github.com/withastro/astro/commit/75781643a2f53656fc3fde3a7f28cb62db40b015) Thanks [@helloimalastair](https://github.com/helloimalastair)! - fixes `AdvancedRuntime` & `DirectoryRuntime` types to work with Cloudflare caches - Updated dependencies [[`2993055be`](https://github.com/withastro/astro/commit/2993055bed2764c31ff4b4f55b81ab6b1ae6b401), [`c4270e476`](https://github.com/withastro/astro/commit/c4270e47681ee2453f3fea07fed7b238645fd6ea), [`bd5aa1cd3`](https://github.com/withastro/astro/commit/bd5aa1cd35ecbd2784f30dd836ff814684fee02b), [`f369fa250`](https://github.com/withastro/astro/commit/f369fa25055a3497ebaf61c88fb0e8af56c73212), [`391729686`](https://github.com/withastro/astro/commit/391729686bcc8404a7dd48c5987ee380daf3200f), [`f999365b8`](https://github.com/withastro/astro/commit/f999365b8248b8b14f3743e68a42d450d06acff3), [`b2ae9ee0c`](https://github.com/withastro/astro/commit/b2ae9ee0c42b11ffc1d3f070d1d5ac881aef84ed), [`0abff97fe`](https://github.com/withastro/astro/commit/0abff97fed3db14be3c75ff9ece3aab67c4ba783), [`3bef32f81`](https://github.com/withastro/astro/commit/3bef32f81c56bc600ca307f1bd40787e23e625a5)]: - astro@3.3.0 @@ -1819,7 +1819,7 @@ ### Minor Changes -- [#8595](https://github.com/withastro/astro/pull/8595) [`5b0b3c9a8`](https://github.com/withastro/astro/commit/5b0b3c9a8e0c0e6b6c7472b82008ab57985f2a04) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Add support for the following Node.js Runtime APIs, which are availabe in [Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/nodejs) using the `node:` syntax. +- [#8595](https://github.com/withastro/astro/pull/8595) [`5b0b3c9a8`](https://github.com/withastro/astro/commit/5b0b3c9a8e0c0e6b6c7472b82008ab57985f2a04) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Add support for the following Node.js Runtime APIs, which are available in [Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/nodejs) using the `node:` syntax. - assert - AsyncLocalStorage - Buffer @@ -2375,7 +2375,7 @@ ### Patch Changes -- [#5301](https://github.com/withastro/astro/pull/5301) [`a79a37cad`](https://github.com/withastro/astro/commit/a79a37cad549b21f91599ff86899e456d9dcc7df) Thanks [@bluwy](https://github.com/bluwy)! - Fix environment variables usage in worker output and warn if environment variables are accessedd too early +- [#5301](https://github.com/withastro/astro/pull/5301) [`a79a37cad`](https://github.com/withastro/astro/commit/a79a37cad549b21f91599ff86899e456d9dcc7df) Thanks [@bluwy](https://github.com/bluwy)! - Fix environment variables usage in worker output and warn if environment variables are accessed too early - Updated dependencies [[`88c1bbe3a`](https://github.com/withastro/astro/commit/88c1bbe3a71f85e92f42f13d0f310c6b2a264ade), [`a79a37cad`](https://github.com/withastro/astro/commit/a79a37cad549b21f91599ff86899e456d9dcc7df)]: - astro@1.6.5 @@ -2564,7 +2564,7 @@ When using the `"server"` output target, you must also include a runtime adapter via the `adapter` configuration. An adapter will _adapt_ your final build to run on the deployed platform of your choice (Netlify, Vercel, Node.js, Deno, etc). - To migrate: No action is required for most users. If you currently define an `adapter`, you will need to also add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: + To migrate: No action is required for most users. If you currently define an `adapter`, you will also need to add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: ```diff import { defineConfig } from 'astro/config'; diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md index 7190ec946354..f1c41bb34ef3 100644 --- a/packages/integrations/cloudflare/README.md +++ b/packages/integrations/cloudflare/README.md @@ -1,6 +1,6 @@ # @astrojs/cloudflare -An SSR adapter for use with Cloudflare Pages Functions targets. Write your code in Astro/Javascript and deploy to Cloudflare Pages. +An SSR adapter for use with Cloudflare Pages Functions targets. Write your code in Astro/JavaScript and deploy to Cloudflare Pages. ## Documentation diff --git a/packages/integrations/cloudflare/src/utils/handler.ts b/packages/integrations/cloudflare/src/utils/handler.ts index 7ac16f9cf924..b25efc5e0b22 100644 --- a/packages/integrations/cloudflare/src/utils/handler.ts +++ b/packages/integrations/cloudflare/src/utils/handler.ts @@ -16,6 +16,7 @@ import { isStaticImagesRequest, handleStaticImagesRequest, } from './prerender.js'; +import { getValidatedIpFromHeader } from '@astrojs/internal-helpers/request'; setGetEnv(createGetEnv(globalEnv)); @@ -125,7 +126,7 @@ export async function handle( // NOTE this ASSETS binding path is needed for users who are using `run_worker_first` routing return env.ASSETS.fetch(url.replace(/\.html$/, '')); }, - clientAddress: request.headers.get('cf-connecting-ip') ?? undefined, + clientAddress: getValidatedIpFromHeader(request.headers.get('cf-connecting-ip')), }); if (app.setCookieHeaders) { diff --git a/packages/integrations/cloudflare/test/client-address.test.js b/packages/integrations/cloudflare/test/client-address.test.js new file mode 100644 index 000000000000..e905afd61c07 --- /dev/null +++ b/packages/integrations/cloudflare/test/client-address.test.js @@ -0,0 +1,107 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import { loadFixture } from './_test-utils.js'; + +/** + * Tests that the Cloudflare adapter correctly extracts and validates + * clientAddress from the cf-connecting-ip header, ensuring: + * - Only the first value is returned from multi-value headers + * - The value is validated as a syntactically valid IP address + * - Injection payloads are rejected + * + * Regression test for: https://github.com/withastro/astro-security/issues/69 + */ +describe('Cloudflare clientAddress', () => { + let fixture; + let previewServer; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/client-address/', + }); + await fixture.build(); + previewServer = await fixture.preview(); + }); + + after(async () => { + previewServer.stop(); + }); + + it('returns the client IP from cf-connecting-ip header', async () => { + const res = await fixture.fetch('/api/address', { + headers: { 'cf-connecting-ip': '203.0.113.50' }, + }); + assert.equal(res.status, 200); + const data = await res.json(); + assert.equal(data.clientAddress, '203.0.113.50'); + }); + + it('returns only the first IP when cf-connecting-ip contains multiple values', async () => { + const res = await fixture.fetch('/api/address', { + headers: { 'cf-connecting-ip': '203.0.113.50, 70.41.3.18, 150.172.238.178' }, + }); + assert.equal(res.status, 200); + const data = await res.json(); + assert.equal(data.clientAddress, '203.0.113.50'); + }); + + it('trims whitespace around the IP address', async () => { + const res = await fixture.fetch('/api/address', { + headers: { 'cf-connecting-ip': ' 203.0.113.50 ' }, + }); + assert.equal(res.status, 200); + const data = await res.json(); + assert.equal(data.clientAddress, '203.0.113.50'); + }); + + it('handles IPv6 addresses', async () => { + const res = await fixture.fetch('/api/address', { + headers: { 'cf-connecting-ip': '2001:db8::1' }, + }); + assert.equal(res.status, 200); + const data = await res.json(); + assert.equal(data.clientAddress, '2001:db8::1'); + }); + + it('renders the client address in an Astro page', async () => { + const res = await fixture.fetch('/', { + headers: { 'cf-connecting-ip': '198.51.100.42' }, + }); + assert.equal(res.status, 200); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal($('#address').text(), '198.51.100.42'); + }); + + it('renders only the first IP in an Astro page when header has multiple values', async () => { + const res = await fixture.fetch('/', { + headers: { 'cf-connecting-ip': '198.51.100.42, 10.0.0.1' }, + }); + assert.equal(res.status, 200); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal($('#address').text(), '198.51.100.42'); + }); + + it('rejects HTML injection in cf-connecting-ip', async () => { + const res = await fixture.fetch('/api/address', { + headers: { 'cf-connecting-ip': '' }, + }); + assert.equal(res.status, 500); + }); + + it('rejects SQL injection in cf-connecting-ip', async () => { + const res = await fixture.fetch('/api/address', { + headers: { 'cf-connecting-ip': "'; DROP TABLE users; --" }, + }); + assert.equal(res.status, 500); + }); + + it('rejects path traversal in cf-connecting-ip', async () => { + const res = await fixture.fetch('/api/address', { + headers: { 'cf-connecting-ip': '../../etc/passwd' }, + }); + assert.equal(res.status, 500); + }); +}); diff --git a/packages/integrations/cloudflare/test/fixtures/client-address/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/client-address/astro.config.mjs new file mode 100644 index 000000000000..339f0e2a49c0 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/client-address/astro.config.mjs @@ -0,0 +1,7 @@ +import cloudflare from '@astrojs/cloudflare'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + adapter: cloudflare(), + output: 'server', +}); diff --git a/packages/integrations/cloudflare/test/fixtures/client-address/package.json b/packages/integrations/cloudflare/test/fixtures/client-address/package.json new file mode 100644 index 000000000000..4ecd2d2bcfb2 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/client-address/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-cloudflare-client-address", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/cloudflare": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/cloudflare/test/fixtures/client-address/src/pages/api/address.ts b/packages/integrations/cloudflare/test/fixtures/client-address/src/pages/api/address.ts new file mode 100644 index 000000000000..4d765c569ac6 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/client-address/src/pages/api/address.ts @@ -0,0 +1,5 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = (ctx) => { + return Response.json({ clientAddress: ctx.clientAddress }); +}; diff --git a/packages/integrations/cloudflare/test/fixtures/client-address/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/client-address/src/pages/index.astro new file mode 100644 index 000000000000..9fb9db9f4b3b --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/client-address/src/pages/index.astro @@ -0,0 +1,11 @@ +--- +const address = Astro.clientAddress; +--- + + + Client Address + + +
{address}
+ + diff --git a/packages/integrations/cloudflare/test/fixtures/client-address/wrangler.jsonc b/packages/integrations/cloudflare/test/fixtures/client-address/wrangler.jsonc new file mode 100644 index 000000000000..b0f325b609e6 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/client-address/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "test-client-address", + "main": "@astrojs/cloudflare/entrypoints/server", + "compatibility_date": "2026-01-28" +} diff --git a/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/pages/index.astro index 132b7ec66607..e16ac3d497ec 100644 --- a/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/pages/index.astro +++ b/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/pages/index.astro @@ -17,7 +17,7 @@ const dogs = await getCollection('dogs'); const increment = await getEntry('increment', 'value'); const { Content } = await render(increment); -const surnamne = Astro.url.searchParams.get('surname'); +const surname = Astro.url.searchParams.get('surname'); --- @@ -51,7 +51,7 @@ const surnamne = Astro.url.searchParams.get('surname'); - +
    {dogs.map(dog => (
  • { dog.data?.breed }
  • diff --git a/packages/integrations/markdoc/src/content-entry-type.ts b/packages/integrations/markdoc/src/content-entry-type.ts index 76e10e6bc9d3..3f0c9b9181e8 100644 --- a/packages/integrations/markdoc/src/content-entry-type.ts +++ b/packages/integrations/markdoc/src/content-entry-type.ts @@ -342,7 +342,7 @@ async function emitOptimizedImages( } } else if (isComponent) { // If the user is using the {% image %} tag, always pass the `src` attribute as `__optimizedSrc`, even if it's an external URL or absolute path. - // That way, the component can decide whether to optimize it or not. + // That way, the component can decide whether or not to optimize it. node.attributes[attributeName] = node.attributes.src; } } diff --git a/packages/integrations/markdoc/src/utils.ts b/packages/integrations/markdoc/src/utils.ts index dc9c9023ff46..6185e6c268b7 100644 --- a/packages/integrations/markdoc/src/utils.ts +++ b/packages/integrations/markdoc/src/utils.ts @@ -19,7 +19,7 @@ export class MarkdocError extends Error { this.title = title; if (message) this.message = message; - // Only set this if we actually have a stack passed, otherwise uses Error's + // Only set this if we actually have a stack passed; otherwise, uses Error's this.stack = stack ? stack : this.stack; this.loc = location; this.hint = hint; diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/components.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/components.mdoc index 55890ce0933a..53de30ad2a7f 100644 --- a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/components.mdoc +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/components.mdoc @@ -2,18 +2,18 @@ title: Welcome to Markdoc 👋 --- -This is a {% mark color="hotpink" %}inline mark{% /mark %} in regular Markdown markup. +This is an {% mark color="hotpink" %}inline mark{% /mark %} in regular Markdown markup. -

    This is a {% mark color="hotpink" %}inline mark{% /mark %} under some HTML

    +

    This is an {% mark color="hotpink" %}inline mark{% /mark %} under some HTML

    -

    This is a {% mark color="hotpink" %}inline mark{% /mark %} under some HTML

    -

    This is a {% mark color="hotpink" %}inline mark{% /mark %} under some HTML

    +

    This is an {% mark color="hotpink" %}inline mark{% /mark %} under some HTML

    +

    This is an {% mark color="hotpink" %}inline mark{% /mark %} under some HTML

    {% aside title="Aside One" type="tip" %} -I'm a Markdown paragraph inside an top-level aside tag +I'm a Markdown paragraph inside a top-level aside tag ## I'm an H2 via Markdown markup diff --git a/packages/integrations/markdoc/test/render-html.test.js b/packages/integrations/markdoc/test/render-html.test.js index 5bf7fe5cef8c..bb5135cccb12 100644 --- a/packages/integrations/markdoc/test/render-html.test.js +++ b/packages/integrations/markdoc/test/render-html.test.js @@ -222,24 +222,24 @@ function renderComponentsHTMLChecks(html) { const { document } = parseHTML(html); const naturalP1 = document.querySelector('article > p:nth-of-type(1)'); - assert.equal(naturalP1.textContent, 'This is a inline mark in regular Markdown markup.'); + assert.equal(naturalP1.textContent, 'This is an inline mark in regular Markdown markup.'); assert.equal(naturalP1.children.length, 1); const p1 = document.querySelector('article > p:nth-of-type(2)'); assert.equal(p1.id, 'p1'); - assert.equal(p1.textContent, 'This is a inline mark under some HTML'); + assert.equal(p1.textContent, 'This is an inline mark under some HTML'); assert.equal(p1.children.length, 1); assertInlineMark(p1.children[0]); const div1p1 = document.querySelector('article > #div1 > p:nth-of-type(1)'); assert.equal(div1p1.id, 'div1-p1'); - assert.equal(div1p1.textContent, 'This is a inline mark under some HTML'); + assert.equal(div1p1.textContent, 'This is an inline mark under some HTML'); assert.equal(div1p1.children.length, 1); assertInlineMark(div1p1.children[0]); const div1p2 = document.querySelector('article > #div1 > p:nth-of-type(2)'); assert.equal(div1p2.id, 'div1-p2'); - assert.equal(div1p2.textContent, 'This is a inline mark under some HTML'); + assert.equal(div1p2.textContent, 'This is an inline mark under some HTML'); assert.equal(div1p2.children.length, 1); const div1p2span1 = div1p2.querySelector('span'); @@ -255,7 +255,7 @@ function renderComponentsHTMLChecks(html) { const aside1SectionP1 = aside1Section.querySelector('p:nth-of-type(1)'); assert.equal( aside1SectionP1.textContent, - "I'm a Markdown paragraph inside an top-level aside tag", + "I'm a Markdown paragraph inside a top-level aside tag", ); const aside1H2_1 = aside1Section.querySelector('h2:nth-of-type(1)'); assert.equal(aside1H2_1.id, 'im-an-h2-via-markdown-markup'); // automatic slug diff --git a/packages/integrations/mdx/CHANGELOG.md b/packages/integrations/mdx/CHANGELOG.md index c5a6842d8a43..cbeb2ab74eca 100644 --- a/packages/integrations/mdx/CHANGELOG.md +++ b/packages/integrations/mdx/CHANGELOG.md @@ -555,7 +555,7 @@ If your integration relies on Astro's previous behavior that prevents integrations from adding remark/rehype plugins for MDX, you will now need to configure `@astrojs/mdx` with `extendMarkdownConfig: false` and explicitly specify any `remarkPlugins` and `rehypePlugins` options instead. -- [#10935](https://github.com/withastro/astro/pull/10935) [`ddd8e49`](https://github.com/withastro/astro/commit/ddd8e49d1a179bec82310fb471f822a1567a6610) Thanks [@bluwy](https://github.com/bluwy)! - Renames the `optimize.customComponentNames` option to `optimize.ignoreElementNames` to better reflect its usecase. Its behaviour is not changed and should continue to work as before. +- [#10935](https://github.com/withastro/astro/pull/10935) [`ddd8e49`](https://github.com/withastro/astro/commit/ddd8e49d1a179bec82310fb471f822a1567a6610) Thanks [@bluwy](https://github.com/bluwy)! - Renames the `optimize.customComponentNames` option to `optimize.ignoreElementNames` to better reflect its use case. Its behaviour is not changed and should continue to work as before. - [#10935](https://github.com/withastro/astro/pull/10935) [`ddd8e49`](https://github.com/withastro/astro/commit/ddd8e49d1a179bec82310fb471f822a1567a6610) Thanks [@bluwy](https://github.com/bluwy)! - Replaces the internal `remark-images-to-component` plugin with `rehype-images-to-component` to let users use additional rehype plugins for images @@ -1198,7 +1198,7 @@ } ``` - This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or reype. + This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or rehype. - [#5891](https://github.com/withastro/astro/pull/5891) [`05caf445d`](https://github.com/withastro/astro/commit/05caf445d4d2728f1010aeb2179a9e756c2fd17d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Remove deprecated Markdown APIs from Astro v0.X. This includes `getHeaders()`, the `.astro` property for layouts, and the `rawContent()` and `compiledContent()` error messages for MDX. @@ -1343,7 +1343,7 @@ } ``` - This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or reype. + This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or rehype. - [#5684](https://github.com/withastro/astro/pull/5684) [`a9c292026`](https://github.com/withastro/astro/commit/a9c2920264e36cc5dc05f4adc1912187979edb0d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Refine Markdown and MDX configuration options for ease-of-use. diff --git a/packages/integrations/mdx/src/README.md b/packages/integrations/mdx/src/README.md index 42e1d688d91b..2a7aeb2a5897 100644 --- a/packages/integrations/mdx/src/README.md +++ b/packages/integrations/mdx/src/README.md @@ -64,7 +64,7 @@ Flow: A: No, but we'll clear that up later in step 3. 3. For each `node` we leave, pop from `elementStack`. If the `node`'s parent is in `allPossibleElements`, we also remove the `node` from `allPossibleElements`. - Q: Why do we check for the node's parent?
    - A: Checking for the node's parent allows us to identify a subtree root. When we enter a subtree like `C -> D -> E`, we leave in reverse: `E -> D -> C`. When we leave `E`, we see that it's parent `D` exists, so we remove `E`. When we leave `D`, we see `C` exists, so we remove `D`. When we leave `C`, we see that its parent doesn't exist, so we keep `C`, a subtree root. + A: Checking for the node's parent allows us to identify a subtree root. When we enter a subtree like `C -> D -> E`, we leave in reverse: `E -> D -> C`. When we leave `E`, we see that its parent `D` exists, so we remove `E`. When we leave `D`, we see `C` exists, so we remove `D`. When we leave `C`, we see that its parent doesn't exist, so we keep `C`, a subtree root. 4. _(Returning to the code written for step 2's `node` enter handling)_ We also need to handle the case where we find non-static elements. If found, we remove all the elements in `elementStack` from `allPossibleElements`. This happens before the code in step 2. - Q: Why?
    A: Because if the `node` isn't static, that means all its ancestors (`elementStack`) have non-static children. So, the ancestors couldn't be a subtree root to be optimized anymore. diff --git a/packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-syntax-highlighting/src/pages/index.mdx similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-syntax-hightlighting/src/pages/index.mdx rename to packages/integrations/mdx/test/fixtures/mdx-syntax-highlighting/src/pages/index.mdx diff --git a/packages/integrations/mdx/test/mdx-plus-react.test.js b/packages/integrations/mdx/test/mdx-plus-react.test.js index ced632466e2f..87f420fc06ef 100644 --- a/packages/integrations/mdx/test/mdx-plus-react.test.js +++ b/packages/integrations/mdx/test/mdx-plus-react.test.js @@ -43,7 +43,7 @@ describe('MDX and React', () => { assert.equal(h.textContent, 'Testing'); }); - it('does not get a invalid hook call warning', () => { + it('does not get an invalid hook call warning', () => { const errors = unhook(); assert.equal(errors.length === 0, true); }); diff --git a/packages/integrations/mdx/test/mdx-script-style-raw.test.js b/packages/integrations/mdx/test/mdx-script-style-raw.test.js index dc4f337bf9ec..3b0acefe04b3 100644 --- a/packages/integrations/mdx/test/mdx-script-style-raw.test.js +++ b/packages/integrations/mdx/test/mdx-script-style-raw.test.js @@ -23,7 +23,7 @@ describe('MDX script style raw', () => { await devServer.stop(); }); - it('works with with raw script and style strings', async () => { + it('works with raw script and style strings', async () => { const res = await fixture.fetch('/index.html'); assert.equal(res.status, 200); @@ -47,7 +47,7 @@ describe('MDX script style raw', () => { }); describe('build', () => { - it('works with with raw script and style strings', async () => { + it('works with raw script and style strings', async () => { const fixture = await loadFixture({ root: FIXTURE_ROOT, integrations: [mdx()], diff --git a/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js b/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js index d0e6a91d0aae..2f72d4eb2866 100644 --- a/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js +++ b/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js @@ -7,7 +7,7 @@ import { parseHTML } from 'linkedom'; import rehypePrettyCode from 'rehype-pretty-code'; import { loadFixture } from '../../../astro/test/test-utils.js'; -const FIXTURE_ROOT = new URL('./fixtures/mdx-syntax-hightlighting/', import.meta.url); +const FIXTURE_ROOT = new URL('./fixtures/mdx-syntax-highlighting/', import.meta.url); describe('MDX syntax highlighting', () => { describe('shiki', () => { diff --git a/packages/integrations/netlify/CHANGELOG.md b/packages/integrations/netlify/CHANGELOG.md index f0d82f9f8799..2c1d5367a0f7 100644 --- a/packages/integrations/netlify/CHANGELOG.md +++ b/packages/integrations/netlify/CHANGELOG.md @@ -1076,7 +1076,7 @@ While not required for fully static, prerendered web sites, you may still wish t }); ``` - This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serveless + Edge Middleware, we are removing the Edge adapter. + This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serverless + Edge Middleware, we are removing the Edge adapter. ### Minor Changes @@ -1178,7 +1178,7 @@ While not required for fully static, prerendered web sites, you may still wish t }); ``` - This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serveless + Edge Middleware, we are removing the Edge adapter. + This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serverless + Edge Middleware, we are removing the Edge adapter. ### Patch Changes @@ -1721,7 +1721,7 @@ While not required for fully static, prerendered web sites, you may still wish t When using the `"server"` output target, you must also include a runtime adapter via the `adapter` configuration. An adapter will _adapt_ your final build to run on the deployed platform of your choice (Netlify, Vercel, Node.js, Deno, etc). - To migrate: No action is required for most users. If you currently define an `adapter`, you will need to also add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: + To migrate: No action is required for most users. If you currently define an `adapter`, you will also need to add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: ```diff import { defineConfig } from 'astro/config'; diff --git a/packages/integrations/netlify/src/index.ts b/packages/integrations/netlify/src/index.ts index 8e1a458a7a12..652d45fc4107 100644 --- a/packages/integrations/netlify/src/index.ts +++ b/packages/integrations/netlify/src/index.ts @@ -502,8 +502,8 @@ export default function netlifyIntegration( plugins: [ { name: 'allowNodePrefixedImports', - setup(puglinBuild) { - puglinBuild.onResolve({ filter: /^node:.*$/ }, (args) => ({ + setup(pluginBuild) { + pluginBuild.onResolve({ filter: /^node:.*$/ }, (args) => ({ path: args.path, external: true, })); diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md index 091745d317e1..e26dc2593c58 100644 --- a/packages/integrations/node/CHANGELOG.md +++ b/packages/integrations/node/CHANGELOG.md @@ -169,7 +169,7 @@ ### Patch Changes -- [#15196](https://github.com/withastro/astro/pull/15196) [`a8317c1`](https://github.com/withastro/astro/commit/a8317c1e1fe72ff3b86890801f5e898a5244c1b0) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where some prendered pages weren't correctly rendered when using the Node.js adapter in middleware mode. +- [#15196](https://github.com/withastro/astro/pull/15196) [`a8317c1`](https://github.com/withastro/astro/commit/a8317c1e1fe72ff3b86890801f5e898a5244c1b0) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where some prerendered pages weren't correctly rendered when using the Node.js adapter in middleware mode. - [#15169](https://github.com/withastro/astro/pull/15169) [`b803d8b`](https://github.com/withastro/astro/commit/b803d8b4b4e5e71ef4b28b23186e2786dc80a308) Thanks [@rururux](https://github.com/rururux)! - fix: fix image 500 error when moving dist directory in standalone Node @@ -190,7 +190,7 @@ This update ensures that configured redirects are always handled as HTTP redirects regardless of output mode, and the default HTML files for the redirects are no longer generated in `static` output. It makes the Node.js adapter more consistent with the other official adapters. - No change to your project is required to take advantage of this new adapter functionality. It is not expected to cause any breaking changes. However, if you relied on the previous redirecting behavior, you may need to handle your redirects differently now. Otherwise you should notice smoother redirects, with more accurate HTTP status codes, and may potentially see some SEO gains. + No change to your project is required to take advantage of this new adapter functionality. It is not expected to cause any breaking changes. However, if you relied on the previous redirecting behavior, you may need to handle your redirects differently now. Otherwise, you should notice smoother redirects, with more accurate HTTP status codes, and may potentially see some SEO gains. ## 9.4.6 @@ -630,7 +630,7 @@ ### Major Changes -- [#9661](https://github.com/withastro/astro/pull/9661) [`d6edc7540864cf5d294d7b881eb886a3804f6d05`](https://github.com/withastro/astro/commit/d6edc7540864cf5d294d7b881eb886a3804f6d05) Thanks [@ematipico](https://github.com/ematipico)! - If host is unset in standalone mode, the server host will now fallback to `localhost` instead of `127.0.0.1`. When `localhost` is used, the operating system can decide to use either `::1` (ipv6) or `127.0.0.1` (ipv4) itself. This aligns with how the Astro dev and preview server works by default. +- [#9661](https://github.com/withastro/astro/pull/9661) [`d6edc7540864cf5d294d7b881eb886a3804f6d05`](https://github.com/withastro/astro/commit/d6edc7540864cf5d294d7b881eb886a3804f6d05) Thanks [@ematipico](https://github.com/ematipico)! - If host is unset in standalone mode, the server host will now fall back to `localhost` instead of `127.0.0.1`. When `localhost` is used, the operating system can decide to use either `::1` (ipv6) or `127.0.0.1` (ipv4) itself. This aligns with how the Astro dev and preview server works by default. If you relied on `127.0.0.1` (ipv4) before, you can set the `HOST` environment variable to `127.0.0.1` to explicitly use ipv4. For example, `HOST=127.0.0.1 node ./dist/server/entry.mjs`. @@ -862,7 +862,7 @@ ### Patch Changes -- [#7708](https://github.com/withastro/astro/pull/7708) [`4dd6c7900`](https://github.com/withastro/astro/commit/4dd6c7900ca40db1b2cebed9bd02a9eb00874d8d) Thanks [@DixCouleur](https://github.com/DixCouleur)! - fix issuse #7590 "res.writeHead is not a function" in Express/Node middleware +- [#7708](https://github.com/withastro/astro/pull/7708) [`4dd6c7900`](https://github.com/withastro/astro/commit/4dd6c7900ca40db1b2cebed9bd02a9eb00874d8d) Thanks [@DixCouleur](https://github.com/DixCouleur)! - fix issue #7590 "res.writeHead is not a function" in Express/Node middleware - Updated dependencies [[`41afb8405`](https://github.com/withastro/astro/commit/41afb84057f606b0e7f9a73c1e40487068e43948), [`c00b6f0c4`](https://github.com/withastro/astro/commit/c00b6f0c49027125ea3026e89b21fef84380d187), [`1f0ee494a`](https://github.com/withastro/astro/commit/1f0ee494a5190356d130282f1f51ba2a5e6ea63f), [`00cb28f49`](https://github.com/withastro/astro/commit/00cb28f4964a60bc609770108d491acc277997b9), [`c264be349`](https://github.com/withastro/astro/commit/c264be3497db4aa8b3bcce0d2f79a26e35b8e91e), [`e1e958a75`](https://github.com/withastro/astro/commit/e1e958a75860292688569e82b4617fc141056202)]: - astro@2.10.0 @@ -1001,7 +1001,7 @@ ### Patch Changes -- [#6088](https://github.com/withastro/astro/pull/6088) [`6a03649f0`](https://github.com/withastro/astro/commit/6a03649f0084f0df6738236d4a86c9936325cee7) Thanks [@QingXia-Ela](https://github.com/QingXia-Ela)! - fix incorrent encoded when path has other language characters +- [#6088](https://github.com/withastro/astro/pull/6088) [`6a03649f0`](https://github.com/withastro/astro/commit/6a03649f0084f0df6738236d4a86c9936325cee7) Thanks [@QingXia-Ela](https://github.com/QingXia-Ela)! - fix incorrect encoded when path has other language characters ## 5.0.1 @@ -1349,7 +1349,7 @@ When using the `"server"` output target, you must also include a runtime adapter via the `adapter` configuration. An adapter will _adapt_ your final build to run on the deployed platform of your choice (Netlify, Vercel, Node.js, Deno, etc). - To migrate: No action is required for most users. If you currently define an `adapter`, you will need to also add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: + To migrate: No action is required for most users. If you currently define an `adapter`, you will also need to add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: ```diff import { defineConfig } from 'astro/config'; diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts index 31547844098c..7a2e4c767afd 100644 --- a/packages/integrations/node/src/index.ts +++ b/packages/integrations/node/src/index.ts @@ -78,6 +78,7 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr host: _config.server.host, port: _config.server.port, staticHeaders: userOptions.staticHeaders ?? false, + bodySizeLimit: userOptions.bodySizeLimit ?? 1024 * 1024 * 1024, }), ], }, diff --git a/packages/integrations/node/src/serve-app.ts b/packages/integrations/node/src/serve-app.ts index 3f646e9d7845..94dc29940e7a 100644 --- a/packages/integrations/node/src/serve-app.ts +++ b/packages/integrations/node/src/serve-app.ts @@ -75,11 +75,18 @@ export function createAppHandler(app: BaseApp, options: Options): RequestHandler return new Response(null, { status: 404 }); }; + // Use the configured body size limit. A value of 0 or Infinity disables the limit. + const effectiveBodySizeLimit = + options.bodySizeLimit === 0 || options.bodySizeLimit === Number.POSITIVE_INFINITY + ? undefined + : options.bodySizeLimit; + return async (req, res, next, locals) => { let request: Request; try { request = createRequest(req, { allowedDomains: app.getAllowedDomains?.() ?? [], + bodySizeLimit: effectiveBodySizeLimit, port: options.port, }); } catch (err) { diff --git a/packages/integrations/node/src/serve-static.ts b/packages/integrations/node/src/serve-static.ts index 33c5ef9cc258..2050c6236e71 100644 --- a/packages/integrations/node/src/serve-static.ts +++ b/packages/integrations/node/src/serve-static.ts @@ -120,9 +120,10 @@ export function createStaticHandler( // app.removeBase sometimes returns a path without a leading slash pathname = prependForwardSlash(app.removeBase(pathname)); - const stream = send(req, pathname, { + const normalizedPathname = path.posix.normalize(pathname); + const stream = send(req, normalizedPathname, { root: client, - dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny', + dotfiles: normalizedPathname.startsWith('/.well-known/') ? 'allow' : 'deny', }); let forwardError = false; @@ -139,7 +140,7 @@ export function createStaticHandler( }); stream.on('headers', (_res: ServerResponse) => { // assets in dist/_astro are hashed and should get the immutable header - if (pathname.startsWith(`/${app.manifest.assetsDir}/`)) { + if (normalizedPathname.startsWith(`/${app.manifest.assetsDir}/`)) { // This is the "far future" cache header, used for static files whose name includes their digest hash. // 1 year (31,536,000 seconds) is convention. // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable diff --git a/packages/integrations/node/src/types.ts b/packages/integrations/node/src/types.ts index 9bb7fd5cdec8..4d853c9ebbc3 100644 --- a/packages/integrations/node/src/types.ts +++ b/packages/integrations/node/src/types.ts @@ -20,6 +20,16 @@ export interface UserOptions { * - The CSP header of the static pages is added when CSP support is enabled. */ staticHeaders?: boolean; + + /** + * Maximum allowed request body size in bytes. Requests with bodies larger than + * this limit will throw an error when the body is consumed. + * + * Set to `Infinity` or `0` to disable the limit. + * + * @default {1073741824} 1GB + */ + bodySizeLimit?: number; } export interface Options extends UserOptions { @@ -28,6 +38,7 @@ export interface Options extends UserOptions { server: string; client: string; staticHeaders: boolean; + bodySizeLimit: number; } export type RequestHandler = (...args: RequestHandlerParams) => void | Promise; diff --git a/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden-file b/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden-file new file mode 100644 index 000000000000..1170b6e1930f --- /dev/null +++ b/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden-file @@ -0,0 +1 @@ +should-not-serve diff --git a/packages/integrations/node/test/well-known-locations.test.js b/packages/integrations/node/test/well-known-locations.test.js index 0951d6c27cc9..57082f28ad6e 100644 --- a/packages/integrations/node/test/well-known-locations.test.js +++ b/packages/integrations/node/test/well-known-locations.test.js @@ -1,7 +1,7 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; describe('test URIs beginning with a dot', () => { /** @type {import('./test-utils').Fixture} */ @@ -43,4 +43,42 @@ describe('test URIs beginning with a dot', () => { assert.equal(res.status, 404); }); }); + + describe('dotfile access via unnormalized paths', async () => { + it('denies dotfile access when path contains .well-known/../ traversal', async () => { + const { handler } = await import('./fixtures/well-known-locations/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url: '/.well-known/../.hidden-file', + }); + + handler(req, res); + req.send(); + + await done; + assert.notEqual( + res.statusCode, + 200, + 'dotfile should not be served via .well-known path traversal', + ); + }); + + it('denies dotfolder file access when path contains .well-known/../ traversal', async () => { + const { handler } = await import('./fixtures/well-known-locations/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url: '/.well-known/../.hidden/file.json', + }); + + handler(req, res); + req.send(); + + await done; + assert.notEqual( + res.statusCode, + 200, + 'dotfolder file should not be served via .well-known path traversal', + ); + }); + }); }); diff --git a/packages/integrations/partytown/src/sirv.ts b/packages/integrations/partytown/src/sirv.ts index bf5268f2c644..c1e394441be4 100644 --- a/packages/integrations/partytown/src/sirv.ts +++ b/packages/integrations/partytown/src/sirv.ts @@ -221,7 +221,7 @@ export default function (dir, opts = {}) { try { pathname = decodeURIComponent(pathname); } catch { - /* malform uri */ + /* malformed uri */ } } diff --git a/packages/integrations/preact/src/client.ts b/packages/integrations/preact/src/client.ts index 40475acc8c01..2eae30d28b47 100644 --- a/packages/integrations/preact/src/client.ts +++ b/packages/integrations/preact/src/client.ts @@ -27,7 +27,7 @@ export default (element: HTMLElement) => const mapValue = props[propName][indexOrKeyInProps]; let valueOfSignal = mapValue; - // not an property key + // not a property key if (typeof indexOrKeyInProps !== 'string') { valueOfSignal = mapValue[0]; indexOrKeyInProps = mapValue[1]; diff --git a/packages/integrations/react/CHANGELOG.md b/packages/integrations/react/CHANGELOG.md index 968b19561063..929242e2490b 100644 --- a/packages/integrations/react/CHANGELOG.md +++ b/packages/integrations/react/CHANGELOG.md @@ -318,7 +318,7 @@ ``` - You may also construct form action URLs using string concatenation, or by using the `URL()` constructor, with the an action's `.queryString` property: + You may also construct form action URLs using string concatenation, or by using the `URL()` constructor, with an action's `.queryString` property: ```astro --- diff --git a/packages/integrations/sitemap/CHANGELOG.md b/packages/integrations/sitemap/CHANGELOG.md index 32322466591d..07f5967f4bc5 100644 --- a/packages/integrations/sitemap/CHANGELOG.md +++ b/packages/integrations/sitemap/CHANGELOG.md @@ -242,7 +242,7 @@ - [#10179](https://github.com/withastro/astro/pull/10179) [`6343f6a438d790fa16a0dd268f4a51def4fa0f33`](https://github.com/withastro/astro/commit/6343f6a438d790fa16a0dd268f4a51def4fa0f33) Thanks [@ematipico](https://github.com/ematipico)! - Revert https://github.com/withastro/astro/pull/9846 - The feature to customize the file name of the sitemap was reverted due to some internal issues with one of the dependencies. With an non-deterministic behaviour, the sitemap file was sometime emitted with incorrect syntax. + The feature to customize the file name of the sitemap was reverted due to some internal issues with one of the dependencies. With a non-deterministic behaviour, the sitemap file was sometime emitted with incorrect syntax. - [#9975](https://github.com/withastro/astro/pull/9975) [`ec7d2ebbd96b8c2dfdadaf076bbf7953007536ed`](https://github.com/withastro/astro/commit/ec7d2ebbd96b8c2dfdadaf076bbf7953007536ed) Thanks [@moose96](https://github.com/moose96)! - Fixes URL generation for routes that rest parameters and start with `/` @@ -425,7 +425,7 @@ When using the `"server"` output target, you must also include a runtime adapter via the `adapter` configuration. An adapter will _adapt_ your final build to run on the deployed platform of your choice (Netlify, Vercel, Node.js, Deno, etc). - To migrate: No action is required for most users. If you currently define an `adapter`, you will need to also add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: + To migrate: No action is required for most users. If you currently define an `adapter`, you will also need to add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: ```diff import { defineConfig } from 'astro/config'; diff --git a/packages/integrations/vercel/CHANGELOG.md b/packages/integrations/vercel/CHANGELOG.md index 0886df9ce586..d21d5fa2ad93 100644 --- a/packages/integrations/vercel/CHANGELOG.md +++ b/packages/integrations/vercel/CHANGELOG.md @@ -1106,7 +1106,7 @@ }); ``` - This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serveless + Edge Middleware, we are removing the Edge adapter. + This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serverless + Edge Middleware, we are removing the Edge adapter. - [#8239](https://github.com/withastro/astro/pull/8239) [`52f0837bd`](https://github.com/withastro/astro/commit/52f0837bdeca0b54e07cbf76a7570bd042b98922) Thanks [@matthewp](https://github.com/matthewp)! - Vercel adapter now defaults to `functionPerRoute`. @@ -1248,7 +1248,7 @@ }); ``` - This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serveless + Edge Middleware, we are removing the Edge adapter. + This adapter had several known limitations and compatibility issues that prevented many people from using it in production. To reduce maintenance costs and because we have a better story with Serverless + Edge Middleware, we are removing the Edge adapter. ### Patch Changes @@ -1917,7 +1917,7 @@ When using the `"server"` output target, you must also include a runtime adapter via the `adapter` configuration. An adapter will _adapt_ your final build to run on the deployed platform of your choice (Netlify, Vercel, Node.js, Deno, etc). - To migrate: No action is required for most users. If you currently define an `adapter`, you will need to also add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: + To migrate: No action is required for most users. If you currently define an `adapter`, you will also need to add `output: 'server'` to your config file to make it explicit that you are building a server. Here is an example of what that change would look like for someone deploying to Netlify: ```diff import { defineConfig } from 'astro/config'; diff --git a/packages/integrations/vercel/src/image/build-service.ts b/packages/integrations/vercel/src/image/build-service.ts index 88f6a2d0aae8..48bf80fecf27 100644 --- a/packages/integrations/vercel/src/image/build-service.ts +++ b/packages/integrations/vercel/src/image/build-service.ts @@ -90,7 +90,7 @@ const service: ExternalImageService = { } // Since `widths` and `densities` ultimately control the width and height of the image, - // we don't want the dimensions the user specified, we'll create those ourselves. + // we don't want the dimensions to be user specified, we'll create those ourselves. const { width: transformWidth, height: transformHeight, diff --git a/packages/integrations/vercel/src/image/dev-service.ts b/packages/integrations/vercel/src/image/dev-service.ts index c9702cff9754..c80c8d585051 100644 --- a/packages/integrations/vercel/src/image/dev-service.ts +++ b/packages/integrations/vercel/src/image/dev-service.ts @@ -18,7 +18,7 @@ const service: LocalImageService = { }, transform(inputBuffer, transform, serviceOptions) { // NOTE: Hardcoding webp here isn't accurate to how the Vercel Image Optimization API works, normally what we should - // do is setup a custom endpoint that sniff the user's accept-content header and serve the proper format based on the + // do is set up a custom endpoint that sniff the user's accept-content header and serve the proper format based on the // user's Vercel config. However, that's: a lot of work for: not much. The dev service is inaccurate to the prod service // in many more ways, this is one of the less offending cases and is, imo, okay, erika - 2023-04-27 transform.format = transform.src.endsWith('svg') ? 'svg' : 'webp'; diff --git a/packages/integrations/vercel/src/image/shared.ts b/packages/integrations/vercel/src/image/shared.ts index c3f135fe5bb3..2e39699924b1 100644 --- a/packages/integrations/vercel/src/image/shared.ts +++ b/packages/integrations/vercel/src/image/shared.ts @@ -116,15 +116,15 @@ export function sharedValidateOptions( // The logic for finding the perfect width is a bit confusing, here it goes: // For images where no width has been specified: - // - For local, imported images, fallback to nearest width we can find in our configured + // - For local, imported images, fall back to nearest width we can find in our configured // - For remote images, that's an error, width is always required. // For images where a width has been specified: - // - If the width that the user asked for isn't in `sizes`, then fallback to the nearest one, but save the width + // - If the width that the user asked for isn't in `sizes`, then fall back to the nearest one, but save the width // the user asked for so we can put it on the `img` tag later. // - Otherwise, just use as-is. // The end goal is: - // - The size on the page is always the one the user asked for or the base image's size - // - The actual size of the image file is always one of `sizes`, either the one the user asked for or the nearest to it + // - The size on the page is always the one that the user asked for or the base image's size + // - The actual size of the image file is always one of `sizes`, either the one that the user asked for or the nearest to it if (!options.width) { const src = options.src; if (isESMImportedImage(src)) { @@ -144,7 +144,7 @@ export function sharedValidateOptions( return Math.abs(curr - options.width!) < Math.abs(prev - options.width!) ? curr : prev; }); - // Save the width the user asked for to inform the `width` and `height` on the `img` tag + // Save the user's requested width to inform the `width` and `height` on the `img` tag options.inputtedWidth = options.width; options.width = nearestWidth; } diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts index afec811faec7..e0ce65cc5551 100644 --- a/packages/integrations/vercel/src/serverless/entrypoint.ts +++ b/packages/integrations/vercel/src/serverless/entrypoint.ts @@ -7,6 +7,7 @@ import { } from '../index.js'; import { middlewareSecret, skewProtection } from 'virtual:astro-vercel:config'; import { createApp } from 'astro/app/entrypoint'; +import { getClientIpAddress } from '@astrojs/internal-helpers/request'; setGetEnv((key) => process.env[key]); @@ -48,7 +49,7 @@ export default { const response = await app.render(request, { routeData, - clientAddress: request.headers.get('x-forwarded-for') ?? undefined, + clientAddress: getClientIpAddress(request), locals, }); diff --git a/packages/integrations/vercel/test/static.test.js b/packages/integrations/vercel/test/static.test.js index 20507f864211..8702cf52d8db 100644 --- a/packages/integrations/vercel/test/static.test.js +++ b/packages/integrations/vercel/test/static.test.js @@ -15,7 +15,7 @@ describe('static routing', () => { it('falls back to 404.html', async () => { const deploymentConfig = JSON.parse(await fixture.readFile('../.vercel/output/config.json')); - // change the index if necesseary + // change the index if necessary assert.deepEqual(deploymentConfig.routes[2], { src: '^/.*$', dest: '/404.html', diff --git a/packages/internal-helpers/package.json b/packages/internal-helpers/package.json index 89afb6abef92..8951d5e53d0d 100644 --- a/packages/internal-helpers/package.json +++ b/packages/internal-helpers/package.json @@ -16,7 +16,8 @@ "./remote": "./dist/remote.js", "./fs": "./dist/fs.js", "./cli": "./dist/cli.js", - "./create-filter": "./dist/create-filter.js" + "./create-filter": "./dist/create-filter.js", + "./request": "./dist/request.js" }, "typesVersions": { "*": { diff --git a/packages/internal-helpers/src/path.ts b/packages/internal-helpers/src/path.ts index 060c8ca4e4d5..11b4c843bf21 100644 --- a/packages/internal-helpers/src/path.ts +++ b/packages/internal-helpers/src/path.ts @@ -24,6 +24,15 @@ export function collapseDuplicateLeadingSlashes(path: string) { return path.replace(MANY_LEADING_SLASHES, '/'); } +const MANY_SLASHES = /\/{2,}/g; + +export function collapseDuplicateSlashes(path: string) { + if (!path) { + return path; + } + return path.replace(MANY_SLASHES, '/'); +} + export const MANY_TRAILING_SLASHES = /\/{2,}$/g; export function collapseDuplicateTrailingSlashes(path: string, trailingSlash: boolean) { diff --git a/packages/internal-helpers/src/request.ts b/packages/internal-helpers/src/request.ts new file mode 100644 index 000000000000..45082b82b24b --- /dev/null +++ b/packages/internal-helpers/src/request.ts @@ -0,0 +1,62 @@ +/** + * Utilities for extracting information from `Request` + */ + +// Parses multiple header and returns first value if available. +export function getFirstForwardedValue(multiValueHeader?: string | string[] | null) { + return multiValueHeader + ?.toString() + ?.split(',') + .map((e) => e.trim())?.[0]; +} + +// Character-allowlist for IP addresses. Rejects injection payloads (HTML, SQL, +// path traversal, etc.) while accepting any well-formed IPv4/IPv6 string. +// +// Allowed characters: +// 0-9 digits (IPv4 octets, IPv6 groups) +// a-fA-F hex digits (IPv6) +// . dot separator (IPv4, IPv4-mapped IPv6) +// : colon separator (IPv6) +// +// Max length 45 covers the longest valid representation +// (full IPv6 with IPv4-mapped suffix is 45 chars). +const IP_RE = /^[0-9a-fA-F.:]{1,45}$/; + +/** + * Checks whether a string looks like an IP address (contains only characters + * that can appear in IPv4/IPv6 addresses and is within a reasonable length). + * + * This is a permissive allowlist — it won't catch every malformed IP, but it + * reliably rejects injection payloads. Does NOT use Node.js APIs so it works + * in all runtimes (Workers, Deno, etc.). + */ +export function isValidIpAddress(value: string): boolean { + return IP_RE.test(value); +} + +/** + * Extracts the first value from a potentially multi-value header and validates + * that it is a syntactically valid IP address. + * + * Useful for adapters that read client IP from a platform-specific header + */ +export function getValidatedIpFromHeader( + headerValue: string | string[] | null | undefined, +): string | undefined { + const raw = getFirstForwardedValue(headerValue); + if (raw && isValidIpAddress(raw)) { + return raw; + } + return undefined; +} + +/** + * Returns the first value associated to the `x-forwarded-for` header, + * but only if it is a valid IP address. Returns `undefined` otherwise. + * + * @param {Request} request + */ +export function getClientIpAddress(request: Request): string | undefined { + return getValidatedIpFromHeader(request.headers.get('x-forwarded-for')); +} diff --git a/packages/internal-helpers/test/request.test.js b/packages/internal-helpers/test/request.test.js new file mode 100644 index 000000000000..8741dbdddb62 --- /dev/null +++ b/packages/internal-helpers/test/request.test.js @@ -0,0 +1,223 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + getClientIpAddress, + getFirstForwardedValue, + getValidatedIpFromHeader, + isValidIpAddress, +} from '../dist/request.js'; + +describe('getFirstForwardedValue', () => { + it('should return the first value from a comma-separated string', () => { + assert.equal(getFirstForwardedValue('a, b, c'), 'a'); + }); + + it('should return the only value when there is a single value', () => { + assert.equal(getFirstForwardedValue('203.0.113.50'), '203.0.113.50'); + }); + + it('should trim whitespace from the returned value', () => { + assert.equal(getFirstForwardedValue(' 203.0.113.50 , 10.0.0.1 '), '203.0.113.50'); + }); + + it('should return undefined for undefined input', () => { + assert.equal(getFirstForwardedValue(undefined), undefined); + }); + + it('should return undefined for null input', () => { + assert.equal(getFirstForwardedValue(null), undefined); + }); + + it('should handle an empty string', () => { + assert.equal(getFirstForwardedValue(''), ''); + }); + + it('should handle a string array by joining and splitting', () => { + // .toString() on a string array produces "a,b", then split(',') works + assert.equal(getFirstForwardedValue(['203.0.113.50', '10.0.0.1']), '203.0.113.50'); + }); + + it('should handle an IPv6 address', () => { + assert.equal(getFirstForwardedValue('2001:db8::1'), '2001:db8::1'); + }); +}); + +describe('isValidIpAddress', () => { + const validAddresses = [ + // IPv4 + '127.0.0.1', + '0.0.0.0', + '255.255.255.255', + '192.168.1.1', + '10.0.0.1', + '203.0.113.50', + + // IPv6 + '::1', + '::', + '2001:db8::1', + 'fe80::1', + '::ffff:192.0.2.1', + '2001:0db8:0000:0000:0000:0000:0000:0001', + 'fd12:3456:789a::1', + ]; + + const invalidAddresses = [ + // Injection payloads + '', + "'; DROP TABLE users; --", + '../../etc/passwd', + '', + + // Arbitrary strings + 'not-an-ip', + 'hello world', + 'localhost', + 'example.com', + + // Empty / whitespace + '', + ' ', + + // Oversized + '1'.repeat(46), + + // Path-like + '/home/user', + 'C:\\Windows', + + // URL-like + 'http://evil.com', + ]; + + it('should accept valid IP addresses', () => { + for (const addr of validAddresses) { + assert.equal(isValidIpAddress(addr), true, `Expected "${addr}" to be valid`); + } + }); + + it('should reject non-IP strings', () => { + for (const addr of invalidAddresses) { + assert.equal(isValidIpAddress(addr), false, `Expected "${addr}" to be invalid`); + } + }); +}); + +describe('getValidatedIpFromHeader', () => { + it('should return a valid IP from a single-value header', () => { + assert.equal(getValidatedIpFromHeader('203.0.113.50'), '203.0.113.50'); + }); + + it('should return the first valid IP from a multi-value header', () => { + assert.equal(getValidatedIpFromHeader('203.0.113.50, 10.0.0.1'), '203.0.113.50'); + }); + + it('should return undefined for non-IP header values', () => { + assert.equal(getValidatedIpFromHeader(''), undefined); + }); + + it('should return undefined for null', () => { + assert.equal(getValidatedIpFromHeader(null), undefined); + }); + + it('should return undefined for undefined', () => { + assert.equal(getValidatedIpFromHeader(undefined), undefined); + }); + + it('should return undefined for empty string', () => { + assert.equal(getValidatedIpFromHeader(''), undefined); + }); + + it('should handle IPv6 addresses', () => { + assert.equal(getValidatedIpFromHeader('2001:db8::1'), '2001:db8::1'); + }); +}); + +describe('getClientIpAddress', () => { + /** + * Helper to create a minimal Request with given headers. + */ + function makeRequest(headers = {}) { + return new Request('https://example.com', { headers }); + } + + it('should return the IP when x-forwarded-for contains a single address', () => { + const request = makeRequest({ 'x-forwarded-for': '203.0.113.50' }); + assert.equal(getClientIpAddress(request), '203.0.113.50'); + }); + + it('should return the first IP when x-forwarded-for contains multiple addresses', () => { + const request = makeRequest({ + 'x-forwarded-for': '203.0.113.50, 70.41.3.18, 150.172.238.178', + }); + assert.equal(getClientIpAddress(request), '203.0.113.50'); + }); + + it('should trim whitespace around addresses', () => { + const request = makeRequest({ 'x-forwarded-for': ' 203.0.113.50 , 70.41.3.18 ' }); + assert.equal(getClientIpAddress(request), '203.0.113.50'); + }); + + it('should return undefined when x-forwarded-for header is absent', () => { + const request = makeRequest(); + assert.equal(getClientIpAddress(request), undefined); + }); + + it('should return undefined when x-forwarded-for header is empty', () => { + const request = makeRequest({ 'x-forwarded-for': '' }); + assert.equal(getClientIpAddress(request), undefined); + }); + + it('should handle an IPv6 address', () => { + const request = makeRequest({ 'x-forwarded-for': '2001:db8::1' }); + assert.equal(getClientIpAddress(request), '2001:db8::1'); + }); + + it('should return the first IPv6 address from a mixed list', () => { + const request = makeRequest({ 'x-forwarded-for': '2001:db8::1, 203.0.113.50' }); + assert.equal(getClientIpAddress(request), '2001:db8::1'); + }); + + it('should handle IPv4-mapped IPv6 address', () => { + const request = makeRequest({ 'x-forwarded-for': '::ffff:192.0.2.1' }); + assert.equal(getClientIpAddress(request), '::ffff:192.0.2.1'); + }); + + it('should handle the loopback address', () => { + const request = makeRequest({ 'x-forwarded-for': '127.0.0.1' }); + assert.equal(getClientIpAddress(request), '127.0.0.1'); + }); + + it('should return undefined for whitespace-only values', () => { + const request = makeRequest({ 'x-forwarded-for': ' , ' }); + assert.equal(getClientIpAddress(request), undefined); + }); + + it('should not be affected by other headers', () => { + const request = makeRequest({ + 'x-real-ip': '10.0.0.1', + forwarded: 'for=10.0.0.2', + }); + assert.equal(getClientIpAddress(request), undefined); + }); + + it('should reject HTML injection in x-forwarded-for', () => { + const request = makeRequest({ 'x-forwarded-for': '' }); + assert.equal(getClientIpAddress(request), undefined); + }); + + it('should reject SQL injection in x-forwarded-for', () => { + const request = makeRequest({ 'x-forwarded-for': "'; DROP TABLE users; --" }); + assert.equal(getClientIpAddress(request), undefined); + }); + + it('should reject path traversal in x-forwarded-for', () => { + const request = makeRequest({ 'x-forwarded-for': '../../etc/passwd' }); + assert.equal(getClientIpAddress(request), undefined); + }); + + it('should reject oversized x-forwarded-for values', () => { + const request = makeRequest({ 'x-forwarded-for': '1'.repeat(100) }); + assert.equal(getClientIpAddress(request), undefined); + }); +}); diff --git a/packages/language-tools/language-server/CHANGELOG.md b/packages/language-tools/language-server/CHANGELOG.md index ca19702a76e9..62b8ee938609 100644 --- a/packages/language-tools/language-server/CHANGELOG.md +++ b/packages/language-tools/language-server/CHANGELOG.md @@ -655,7 +655,7 @@ In the background, this update means that we now have to maintain a lot less cod ### Patch Changes -- c4f7a36: Trying desesperately to figure deployment out +- c4f7a36: Trying desperately to figure deployment out ## 0.26.0 @@ -717,7 +717,7 @@ In the background, this update means that we now have to maintain a lot less cod ### Patch Changes - 61620f1: Add support for Go To Type Definition -- 9337f00: Fix language server not working when no initlizationOptions were passed +- 9337f00: Fix language server not working when no initializationOptions were passed ## 0.21.1 @@ -748,7 +748,7 @@ In the background, this update means that we now have to maintain a lot less cod ### Patch Changes - e6996f5: Fixed many situations where the language server would warn abusively about not being able to find Astro -- 4589c2b: Fix the language server not warning properly when a package is implicitely any due to missing types +- 4589c2b: Fix the language server not warning properly when a package is implicitly any due to missing types ## 0.20.0 @@ -772,7 +772,7 @@ In the background, this update means that we now have to maintain a lot less cod - 421ab52: Added a new setting (`astro.typescript.allowArbitraryAttributes`) to enable support for arbitrary attributes - 06e3c95: Updated behaviour when no settings are provided. All features are now considered enabled by default -- 301dcfb: Remove Lodash from the code base, significally reducing the file count of the package +- 301dcfb: Remove Lodash from the code base, significantly reducing the file count of the package - dd1283b: Updated Component detection so completions now work for namespaced components (for example, typing `text` + // If the edit is at the start of the file, add a newline before it; otherwise, we'll get `