diff options
108 files changed, 4531 insertions, 6 deletions
diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md index 8aa72afef..c9f7e6349 100644 --- a/packages/integrations/node/CHANGELOG.md +++ b/packages/integrations/node/CHANGELOG.md @@ -1,6 +1,62 @@ # @astrojs/node -## 1.0.0 +## 9.0.2 + +### Patch Changes + +- [#514](https://github.com/withastro/adapters/pull/514) [`ea4297b`](https://github.com/withastro/adapters/commit/ea4297b7bdb72ef0202e9f547625e7fa71a6a73e) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a bug that caused the preview server to ignore wildcard host options + +## 9.0.1 + +### Patch Changes + +- [#454](https://github.com/withastro/adapters/pull/454) [`83cedad`](https://github.com/withastro/adapters/commit/83cedad780bf7a23ae9f6ca0c44a7b7f1c1767e1) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Improves Astro 5 support + +## 9.0.0 + +### Major Changes + +- [#375](https://github.com/withastro/adapters/pull/375) [`e7881f7`](https://github.com/withastro/adapters/commit/e7881f7928c6ca62d43c763033f9ed065a907f3b) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updates internal code to works with Astro 5 changes to hybrid rendering. No changes are necessary to your project, apart from using Astro 5 + +- [#397](https://github.com/withastro/adapters/pull/397) [`776a266`](https://github.com/withastro/adapters/commit/776a26670cf483e37ec0e6eba27a0bde09db0146) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Welcome to the Astro 5 beta! This release has no changes from the latest alpha of this package, but it does bring us one step closer to the final, stable release. + + Starting from this release, no breaking changes will be introduced unless absolutely necessary. + + To learn how to upgrade, check out the [Astro v5.0 upgrade guide in our beta docs site](https://5-0-0-beta.docs.astro.build/en/guides/upgrade-to/v5/). + +- [#392](https://github.com/withastro/adapters/pull/392) [`3a49eb7`](https://github.com/withastro/adapters/commit/3a49eb7802c44212ccfab06034b7dc5f2b060e94) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updates internal code for Astro 5 changes. No changes is required to your project, apart from using Astro 5 + +- [#451](https://github.com/withastro/adapters/pull/451) [`167b369`](https://github.com/withastro/adapters/commit/167b369a0a1612c792af8846f6ea167e999e1abb) Thanks [@ematipico](https://github.com/ematipico)! - Updates `send` dependency to v1.1.0 + +### Minor Changes + +- [#385](https://github.com/withastro/adapters/pull/385) [`bb725b7`](https://github.com/withastro/adapters/commit/bb725b7a430a01a3cd197e3e84381be4fa0c945c) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Cleans up `astro:env` support + +## 9.0.0-beta.3 + +### Major Changes + +- [`167b369`](https://github.com/withastro/adapters/commit/167b369a0a1612c792af8846f6ea167e999e1abb) Thanks [@bluwy](https://github.com/bluwy)! - Updates `send` dependency to v1.1.0 + +## 9.0.0-beta.2 + +### Major Changes + +- [#375](https://github.com/withastro/adapters/pull/375) [`e7881f7`](https://github.com/withastro/adapters/commit/e7881f7928c6ca62d43c763033f9ed065a907f3b) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updates internal code to works with Astro 5 changes to hybrid rendering. No changes are necessary to your project, apart from using Astro 5 + +- [#397](https://github.com/withastro/adapters/pull/397) [`776a266`](https://github.com/withastro/adapters/commit/776a26670cf483e37ec0e6eba27a0bde09db0146) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Welcome to the Astro 5 beta! This release has no changes from the latest alpha of this package, but it does bring us one step closer to the final, stable release. + + Starting from this release, no breaking changes will be introduced unless absolutely necessary. + + To learn how to upgrade, check out the [Astro v5.0 upgrade guide in our beta docs site](https://5-0-0-beta.docs.astro.build/en/guides/upgrade-to/v5/). + +- [#392](https://github.com/withastro/adapters/pull/392) [`3a49eb7`](https://github.com/withastro/adapters/commit/3a49eb7802c44212ccfab06034b7dc5f2b060e94) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updates internal code for Astro 5 changes. No changes is required to your project, apart from using Astro 5 + +### Minor Changes + +- [#385](https://github.com/withastro/adapters/pull/385) [`bb725b7`](https://github.com/withastro/adapters/commit/bb725b7a430a01a3cd197e3e84381be4fa0c945c) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Cleans up `astro:env` support + +## 9.0.0-alpha.1 ### Major Changes @@ -22,3 +78,920 @@ ``` If you are using this service, and cannot migrate to the base Sharp image service, a third-party extraction of the previous service is available here: https://github.com/Princesseuh/astro-image-service-squoosh + +## 9.0.0-alpha.0 + +### Patch Changes + +- Updated dependencies [[`b6fbdaa`](https://github.com/withastro/astro/commit/b6fbdaa94a9ecec706a99e1938fbf5cd028c72e0), [`89bab1e`](https://github.com/withastro/astro/commit/89bab1e70786123fbe933a9d7a1b80c9334dcc5f), [`d74617c`](https://github.com/withastro/astro/commit/d74617cbd3278feba05909ec83db2d73d57a153e), [`e90f559`](https://github.com/withastro/astro/commit/e90f5593d23043579611452a84b9e18ad2407ef9), [`2df49a6`](https://github.com/withastro/astro/commit/2df49a6fb4f6d92fe45f7429430abe63defeacd6), [`8a53517`](https://github.com/withastro/astro/commit/8a5351737d6a14fc55f1dafad8f3b04079e81af6)]: + - astro@5.0.0-alpha.0 + +## 8.3.4 + +### Patch Changes + +- [#398](https://github.com/withastro/adapters/pull/398) [`0cf7e91`](https://github.com/withastro/adapters/commit/0cf7e912607fcd76072bf710b8f857dc8cc07a33) Thanks [@bluwy](https://github.com/bluwy)! - Updates `send` dependency to 0.19.0 + +## 8.3.4 + +### Patch Changes + +- [#398](https://github.com/withastro/adapters/pull/398) [`0cf7e91`](https://github.com/withastro/adapters/commit/0cf7e912607fcd76072bf710b8f857dc8cc07a33) Thanks [@bluwy](https://github.com/bluwy)! - Updates `send` dependency to 0.19.0 + +## 8.3.3 + +### Patch Changes + +- [#11535](https://github.com/withastro/astro/pull/11535) [`932bd2e`](https://github.com/withastro/astro/commit/932bd2eb07f1d7cb2c91e7e7d31fe84c919e302b) Thanks [@matthewp](https://github.com/matthewp)! - Move polyfills up before awaiting the env module in the Node.js adapter. + + Previously the env setting was happening before the polyfills were applied. This means that if the Astro env code (or any dependencies) depended on `crypto`, it would not be polyfilled in time. + + Polyfills should be applied ASAP to prevent races. This moves it to the top of the Node adapter. + +## 8.3.2 + +### Patch Changes + +- [#11296](https://github.com/withastro/astro/pull/11296) [`5848d97`](https://github.com/withastro/astro/commit/5848d9786768d1290de982670bcc7773280ef08d) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes `astro:env` getSecret compatibility + +## 8.3.1 + +### Patch Changes + +- [#11261](https://github.com/withastro/astro/pull/11261) [`f5f8ed2`](https://github.com/withastro/astro/commit/f5f8ed275b76adfb11b7c3c1e800753a25416498) Thanks [@matthewp](https://github.com/matthewp)! - Fix backwards compat with Astro <= 4.9 + +- [#11263](https://github.com/withastro/astro/pull/11263) [`7d59750`](https://github.com/withastro/astro/commit/7d597506615fa5a34327304e8321be7b9c4b799d) Thanks [@wackbyte](https://github.com/wackbyte)! - Refactor to use Astro's integration logger for logging + +## 8.3.0 + +### Minor Changes + +- [#11199](https://github.com/withastro/astro/pull/11199) [`2bdca27`](https://github.com/withastro/astro/commit/2bdca27ff4002efd330667b0b4ca3e00d5b7a2db) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Adds support for experimental `astro:env` released in Astro 4.10 + +## 8.2.6 + +### Patch Changes + +- [#11202](https://github.com/withastro/astro/pull/11202) [`d0248bc`](https://github.com/withastro/astro/commit/d0248bc083dff713b66d44bcefbf389cdf67e22d) Thanks [@dkobierski](https://github.com/dkobierski)! - Fixes suppressed logs when error occurs + +## 8.2.5 + +### Patch Changes + +- [#10491](https://github.com/withastro/astro/pull/10491) [`28e33a2f9c04373eae5da2e6edb0dc2981bce790`](https://github.com/withastro/astro/commit/28e33a2f9c04373eae5da2e6edb0dc2981bce790) Thanks [@castarco](https://github.com/castarco)! - Fixes a bug where the preview server wrongly appends trailing slashes to subresource URLs. + +## 8.2.4 + +### Patch Changes + +- [#10454](https://github.com/withastro/astro/pull/10454) [`83f9105cd50e2756d02ca2be73ab84f9d582d3f8`](https://github.com/withastro/astro/commit/83f9105cd50e2756d02ca2be73ab84f9d582d3f8) Thanks [@lilnasy](https://github.com/lilnasy)! - Prevents crashes caused by rejections of offshoot promises. + +## 8.2.3 + +### Patch Changes + +- [#10285](https://github.com/withastro/astro/pull/10285) [`d5277df5a4d1e9a8a7b6c8d7b87912e13a163f7f`](https://github.com/withastro/astro/commit/d5277df5a4d1e9a8a7b6c8d7b87912e13a163f7f) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes an issue where malformed requests could cause the server to error in certain cases. + +## 8.2.2 + +### Patch Changes + +- [#10282](https://github.com/withastro/astro/pull/10282) [`b47dcaa25968ec85ba96fce23381c94a94e389f6`](https://github.com/withastro/astro/commit/b47dcaa25968ec85ba96fce23381c94a94e389f6) Thanks [@SatanshuMishra](https://github.com/SatanshuMishra)! - Fixes the `server.host` option to properly listen on all network interfaces when set to `true` + +## 8.2.1 + +### Patch Changes + +- [#10208](https://github.com/withastro/astro/pull/10208) [`8cd38f02456640c063552aef00b2b8a216b3935d`](https://github.com/withastro/astro/commit/8cd38f02456640c063552aef00b2b8a216b3935d) Thanks [@log101](https://github.com/log101)! - Fixes custom headers are not added to the Node standalone server responses in preview mode + +## 8.2.0 + +### Minor Changes + +- [#9143](https://github.com/withastro/astro/pull/9143) [`041fdd5c89920f7ccf944b095f29e451f78b0e28`](https://github.com/withastro/astro/commit/041fdd5c89920f7ccf944b095f29e451f78b0e28) Thanks [@ematipico](https://github.com/ematipico)! - Adds experimental support for internationalization domains + +## 8.1.0 + +### Minor Changes + +- [#9080](https://github.com/withastro/astro/pull/9080) [`a12196d6b59e39f5d405734ecdbf6f6b42b39a93`](https://github.com/withastro/astro/commit/a12196d6b59e39f5d405734ecdbf6f6b42b39a93) Thanks [@msxdan](https://github.com/msxdan)! - Add trailingSlash support to NodeJS adapter + +## 8.0.0 + +### 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. + + 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`. + +- [#9661](https://github.com/withastro/astro/pull/9661) [`d6edc7540864cf5d294d7b881eb886a3804f6d05`](https://github.com/withastro/astro/commit/d6edc7540864cf5d294d7b881eb886a3804f6d05) Thanks [@ematipico](https://github.com/ematipico)! - **Breaking**: Minimum required Astro version is now 4.2.0. + Reorganizes internals to be more maintainable. + +### Patch Changes + +- [#9661](https://github.com/withastro/astro/pull/9661) [`d6edc7540864cf5d294d7b881eb886a3804f6d05`](https://github.com/withastro/astro/commit/d6edc7540864cf5d294d7b881eb886a3804f6d05) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where the preview server appeared to be ready to serve requests before binding to a port. + +## 7.0.4 + +### Patch Changes + +- [#9533](https://github.com/withastro/astro/pull/9533) [`48f47b50a0f8bc0fa51760215def36640f79050d`](https://github.com/withastro/astro/commit/48f47b50a0f8bc0fa51760215def36640f79050d) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes a bug where an error while serving response stopped the server. + +## 7.0.3 + +### Patch Changes + +- [#9479](https://github.com/withastro/astro/pull/9479) [`1baf0b0d3cbd0564954c2366a7278794fad6726e`](https://github.com/withastro/astro/commit/1baf0b0d3cbd0564954c2366a7278794fad6726e) Thanks [@sarah11918](https://github.com/sarah11918)! - Updates README + +## 7.0.2 + +### Patch Changes + +- [#9471](https://github.com/withastro/astro/pull/9471) [`6bf470cfb`](https://github.com/withastro/astro/commit/6bf470cfb87e853c0a1f69bceb09246801bc8bdc) Thanks [@alexnguyennz](https://github.com/alexnguyennz)! - Fix typo in @astrojs/node README + +## 7.0.1 + +### Patch Changes + +- [#9366](https://github.com/withastro/astro/pull/9366) [`1b4e91898`](https://github.com/withastro/astro/commit/1b4e91898116f75b02b66ec402385cf44e559118) Thanks [@lilnasy](https://github.com/lilnasy)! - Updates NPM package to refer to the stable Astro version instead of a beta. + +## 7.0.0 + +### Major Changes + +- [#9199](https://github.com/withastro/astro/pull/9199) [`49aa215a0`](https://github.com/withastro/astro/commit/49aa215a01ee1c4805316c85bb0aea6cfbc25a31) Thanks [@lilnasy](https://github.com/lilnasy)! - The internals of the integration have been updated to support Astro 4.0. Make sure to upgrade your Astro version as Astro 3.0 is no longer supported. + +## 7.0.0-beta.1 + +### Major Changes + +- [#9199](https://github.com/withastro/astro/pull/9199) [`49aa215a0`](https://github.com/withastro/astro/commit/49aa215a01ee1c4805316c85bb0aea6cfbc25a31) Thanks [@lilnasy](https://github.com/lilnasy)! - The internals of the integration have been updated to support Astro 4.0. Make sure to upgrade your Astro version as Astro 3.0 is no longer supported. + +## 7.0.0-beta.0 + +### Patch Changes + +- Updated dependencies [[`abf601233`](https://github.com/withastro/astro/commit/abf601233f8188d118a8cb063c777478d8d9f1a3), [`6201bbe96`](https://github.com/withastro/astro/commit/6201bbe96c2a083fb201e4a43a9bd88499821a3e), [`cdabf6ef0`](https://github.com/withastro/astro/commit/cdabf6ef02be7220fd2b6bdcef924ceca089381e), [`1c48ed286`](https://github.com/withastro/astro/commit/1c48ed286538ab9e354eca4e4dcd7c6385c96721), [`37697a2c5`](https://github.com/withastro/astro/commit/37697a2c5511572dc29c0a4ea46f90c2f62be8e6), [`bd0c2e9ae`](https://github.com/withastro/astro/commit/bd0c2e9ae3389a9d3085050c1e8134ae98dff299), [`0fe3a7ed5`](https://github.com/withastro/astro/commit/0fe3a7ed5d7bb1a9fce1623e84ba14104b51223c), [`710be505c`](https://github.com/withastro/astro/commit/710be505c9ddf416e77a75343d8cae9c497d72c6), [`153a5abb9`](https://github.com/withastro/astro/commit/153a5abb905042ac68b712514dc9ec387d3e6b17)]: + - astro@4.0.0-beta.0 + +## 6.1.0 + +### Minor Changes + +- [#9125](https://github.com/withastro/astro/pull/9125) [`8f1d50957`](https://github.com/withastro/astro/commit/8f1d509574f5ee5d77816a13d89ce452dce403ff) Thanks [@matthewp](https://github.com/matthewp)! - Automatically sets immutable cache headers for assets served from the `/_astro` directory. + +## 6.1.0 + +### Minor Changes + +- [#9125](https://github.com/withastro/astro/pull/9125) [`8f1d50957`](https://github.com/withastro/astro/commit/8f1d509574f5ee5d77816a13d89ce452dce403ff) Thanks [@matthewp](https://github.com/matthewp)! - Automatically sets immutable cache headers for assets served from the `/_astro` directory. + +## 6.0.4 + +### Patch Changes + +- [#9071](https://github.com/withastro/astro/pull/9071) [`c9487138d`](https://github.com/withastro/astro/commit/c9487138d6d8fd39c8c8512239b6724cf2b275ff) Thanks [@pilcrowOnPaper](https://github.com/pilcrowOnPaper)! - Fixes a bug where the response stream would not cancel when the connection closed + +## 6.0.3 + +### Patch Changes + +- [#8737](https://github.com/withastro/astro/pull/8737) [`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c) Thanks [@ematipico](https://github.com/ematipico)! - Add provenance statement when publishing the library from CI + +- Updated dependencies [[`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c), [`d78806dfe`](https://github.com/withastro/astro/commit/d78806dfe0301ea7ffe6c7c1f783bd415ac7cda9), [`d1c75fe15`](https://github.com/withastro/astro/commit/d1c75fe158839699c59728cf3a83888e8c72a459), [`aa265d730`](https://github.com/withastro/astro/commit/aa265d73024422967c1b1c68ad268c419c6c798f), [`78adbc443`](https://github.com/withastro/astro/commit/78adbc4433208458291e36713909762e148e1e5d), [`21e0757ea`](https://github.com/withastro/astro/commit/21e0757ea22a57d344c934045ca19db93b684436), [`357270f2a`](https://github.com/withastro/astro/commit/357270f2a3d0bf2aa634ba7e52e9d17618eff4a7)]: + - astro@3.2.3 + +## 6.0.2 + +### Patch Changes + +- [#8698](https://github.com/withastro/astro/pull/8698) [`47ea310f0`](https://github.com/withastro/astro/commit/47ea310f01d06ed1562c790bec348718a2fa8277) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Use a Node-specific image endpoint to resolve images in dev and Node SSR. This should fix many issues related to getting 404 from the \_image endpoint under certain configurations + +- Updated dependencies [[`31c59ad8b`](https://github.com/withastro/astro/commit/31c59ad8b6a72f95c98a306ecf92d198c03110b4), [`47ea310f0`](https://github.com/withastro/astro/commit/47ea310f01d06ed1562c790bec348718a2fa8277), [`345808170`](https://github.com/withastro/astro/commit/345808170fce783ddd3c9a4035a91fa64dcc5f46)]: + - astro@3.2.1 + +## 6.0.1 + +### Patch Changes + +- [#8599](https://github.com/withastro/astro/pull/8599) [`2e1d5f873`](https://github.com/withastro/astro/commit/2e1d5f8739552c3428aa7cbb82811ed2b9b24fdb) Thanks [@lilnasy](https://github.com/lilnasy)! - The node adapter now logs uncaught errors encountered during rendering a page. + +- Updated dependencies [[`bcad715ce`](https://github.com/withastro/astro/commit/bcad715ce67bc73a7927c941d1e7f02a82d638c2), [`bdd267d08`](https://github.com/withastro/astro/commit/bdd267d08937611984d074a2872af11ecf3e1a12), [`e522a5eb4`](https://github.com/withastro/astro/commit/e522a5eb41c7df1e62c307c84cd14d53777439ff), [`ed54d4644`](https://github.com/withastro/astro/commit/ed54d46449accc99ad117d6b0d50a8905e4d65d7), [`70f2a8003`](https://github.com/withastro/astro/commit/70f2a80039d232731f63ea735e896997ec0eac7a), [`4398e9298`](https://github.com/withastro/astro/commit/4398e929877dfadd2067af28413284afdfde9d8b), [`8f8b9069d`](https://github.com/withastro/astro/commit/8f8b9069ddd21cf57d37955ab3a92710492226f5), [`5a988eaf6`](https://github.com/withastro/astro/commit/5a988eaf609ddc1b9609acb0cdc2dda43d10a5c2)]: + - astro@3.1.2 + +## 6.0.0 + +### Major Changes + +- [#8188](https://github.com/withastro/astro/pull/8188) [`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312) Thanks [@ematipico](https://github.com/ematipico)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023. + +- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate + +- [#8188](https://github.com/withastro/astro/pull/8188) [`148e61d24`](https://github.com/withastro/astro/commit/148e61d2492456811f8a3c8daaab1c3429a2ffdc) Thanks [@ematipico](https://github.com/ematipico)! - Reduced the amount of polyfills provided by Astro. Astro will no longer provide (no-op) polyfills for several web apis such as HTMLElement, Image or Document. If you need access to those APIs on the server, we recommend using more proper polyfills available on npm. + +### Minor Changes + +- [#8188](https://github.com/withastro/astro/pull/8188) [`cd2d7e769`](https://github.com/withastro/astro/commit/cd2d7e76981ef9b9013453aa2629838e1e9fd422) Thanks [@ematipico](https://github.com/ematipico)! - Introduced the concept of feature map. A feature map is a list of features that are built-in in Astro, and an Adapter + can tell Astro if it can support it. + + ```ts + import { AstroIntegration } from './astro'; + + function myIntegration(): AstroIntegration { + return { + name: 'astro-awesome-list', + // new feature map + supportedAstroFeatures: { + hybridOutput: 'experimental', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + }, + }; + } + ``` + +### Patch Changes + +- Updated dependencies [[`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312), [`db39206cb`](https://github.com/withastro/astro/commit/db39206cbb85b034859ac416179f141184bb2bff), [`adf9fccfd`](https://github.com/withastro/astro/commit/adf9fccfdda107c2224558f1c2e6a77847ac0a8a), [`0c7b42dc6`](https://github.com/withastro/astro/commit/0c7b42dc6780e687e416137539f55a3a427d1d10), [`46c4c0e05`](https://github.com/withastro/astro/commit/46c4c0e053f830585b9ef229ce1c259df00a80f8), [`364d861bd`](https://github.com/withastro/astro/commit/364d861bd527b8511968e2837728148f090bedef), [`2484dc408`](https://github.com/withastro/astro/commit/2484dc4080e5cd84b9a53648a1de426d7c907be2), [`81545197a`](https://github.com/withastro/astro/commit/81545197a32fd015d763fc386c8b67e0e08b7393), [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7), [`c2c71d90c`](https://github.com/withastro/astro/commit/c2c71d90c264a2524f99e0373ab59015f23ad4b1), [`cd2d7e769`](https://github.com/withastro/astro/commit/cd2d7e76981ef9b9013453aa2629838e1e9fd422), [`80f1494cd`](https://github.com/withastro/astro/commit/80f1494cdaf72e58a420adb4f7c712d4089e1923), [`e45f30293`](https://github.com/withastro/astro/commit/e45f3029340db718b6ed7e91b5d14f5cf14cd71d), [`c0de7a7b0`](https://github.com/withastro/astro/commit/c0de7a7b0f042cd49cbea4f4ac1b2ab6f9fef644), [`65c354969`](https://github.com/withastro/astro/commit/65c354969e6fe0ef6d622e8f4c545e2f717ce8c6), [`3c3100851`](https://github.com/withastro/astro/commit/3c31008519ce68b5b1b1cb23b71fbe0a2d506882), [`34cb20021`](https://github.com/withastro/astro/commit/34cb2002161ba88df6bcb72fecfd12ed867c134b), [`a824863ab`](https://github.com/withastro/astro/commit/a824863ab1c451f4068eac54f28dd240573e1cba), [`44f7a2872`](https://github.com/withastro/astro/commit/44f7a28728c56c04ac377b6e917329f324874043), [`1048aca55`](https://github.com/withastro/astro/commit/1048aca550769415e528016e42b358ffbfd44b61), [`be6bbd2c8`](https://github.com/withastro/astro/commit/be6bbd2c86b9bf5268e765bb937dda00ff15781a), [`9e021a91c`](https://github.com/withastro/astro/commit/9e021a91c57d10809f588dd47968fc0e7f8b4d5c), [`7511a4980`](https://github.com/withastro/astro/commit/7511a4980fd36536464c317de33a5190427f430a), [`c37632a20`](https://github.com/withastro/astro/commit/c37632a20d06164fb97a4c2fc48df6d960398832), [`acf652fc1`](https://github.com/withastro/astro/commit/acf652fc1d5db166231e87e22d0d50444f5556d8), [`42785c7b7`](https://github.com/withastro/astro/commit/42785c7b784b151e6d582570e5d74482129e8eb8), [`8450379db`](https://github.com/withastro/astro/commit/8450379db854fb1eaa9f38f21d65db240bc616cd), [`dbc97b121`](https://github.com/withastro/astro/commit/dbc97b121f42583728f1cdfdbf14575fda943f5b), [`7d2f311d4`](https://github.com/withastro/astro/commit/7d2f311d428e3d1c8c13b9bf2a708d6435713fc2), [`2540feedb`](https://github.com/withastro/astro/commit/2540feedb06785d5a20eecc3668849f147d778d4), [`ea7ff5177`](https://github.com/withastro/astro/commit/ea7ff5177dbcd7b2508cb1eef1b22b8ee1f47079), [`68efd4a8b`](https://github.com/withastro/astro/commit/68efd4a8b29f248397667801465b3152dc98e9a7), [`7bd1b86f8`](https://github.com/withastro/astro/commit/7bd1b86f85c06fdde0a1ed9146d01bac69990671), [`036388f66`](https://github.com/withastro/astro/commit/036388f66dab68ad54b895ed86f9176958dd83c8), [`519a1c4e8`](https://github.com/withastro/astro/commit/519a1c4e8407c7abcb8d879b67a9f4b960652cae), [`1f58a7a1b`](https://github.com/withastro/astro/commit/1f58a7a1bea6888868b689dac94801d554319b02), [`2ae9d37f0`](https://github.com/withastro/astro/commit/2ae9d37f0a9cb21ab288d3c30aecb6d84db87788), [`a8f35777e`](https://github.com/withastro/astro/commit/a8f35777e7e322068a4e2f520c2c9e43ade19e58), [`70f34f5a3`](https://github.com/withastro/astro/commit/70f34f5a355f42526ee9e5355f3de8e510002ea2), [`5208a3c8f`](https://github.com/withastro/astro/commit/5208a3c8fefcec7694857fb344af351f4631fc34), [`84af8ed9d`](https://github.com/withastro/astro/commit/84af8ed9d1e6401c6ebc9c60fe8cddb44d5044b0), [`f003e7364`](https://github.com/withastro/astro/commit/f003e7364317cafdb8589913b26b28e928dd07c9), [`ffc9e2d3d`](https://github.com/withastro/astro/commit/ffc9e2d3de46049bf3d82140ef018f524fb03187), [`732111cdc`](https://github.com/withastro/astro/commit/732111cdce441639db31f40f621df48442d00969), [`0f637c71e`](https://github.com/withastro/astro/commit/0f637c71e511cb4c51712128d217a26c8eee4d40), [`33b8910cf`](https://github.com/withastro/astro/commit/33b8910cfdce5713891c50a84a0a8fe926311710), [`8a5b0c1f3`](https://github.com/withastro/astro/commit/8a5b0c1f3a4be6bb62db66ec70144109ff5b4c59), [`148e61d24`](https://github.com/withastro/astro/commit/148e61d2492456811f8a3c8daaab1c3429a2ffdc), [`e79e3779d`](https://github.com/withastro/astro/commit/e79e3779df0ad35253abcdb931d622847d9adb12), [`632579dc2`](https://github.com/withastro/astro/commit/632579dc2094cc342929261c89e689f0dd358284), [`3674584e0`](https://github.com/withastro/astro/commit/3674584e02b161a698b429ceb66723918fdc56ac), [`1db4e92c1`](https://github.com/withastro/astro/commit/1db4e92c12ed73681217f5cefd39f2f47542f961), [`e7f872e91`](https://github.com/withastro/astro/commit/e7f872e91e852b901cf221a5151077dec64305bf), [`16f09dfff`](https://github.com/withastro/astro/commit/16f09dfff7722fda99dd0412e3006a7a39c80829), [`4477bb41c`](https://github.com/withastro/astro/commit/4477bb41c8ed688785c545731ef5b184b629f4e5), [`55c10d1d5`](https://github.com/withastro/astro/commit/55c10d1d564e805efc3c1a7c48e0d9a1cdf0c7ed), [`3e834293d`](https://github.com/withastro/astro/commit/3e834293d47ab2761a7aa013916e8371871efb7f), [`96beb883a`](https://github.com/withastro/astro/commit/96beb883ad87f8bbf5b2f57e14a743763d2a6f58), [`997a0db8a`](https://github.com/withastro/astro/commit/997a0db8a4e3851edd69384cf5eadbb969e1d547), [`80f1494cd`](https://github.com/withastro/astro/commit/80f1494cdaf72e58a420adb4f7c712d4089e1923), [`0f0625504`](https://github.com/withastro/astro/commit/0f0625504145f18cba7dc6cf20291cb2abddc5a9), [`e1ae56e72`](https://github.com/withastro/astro/commit/e1ae56e724d0f83db1230359e06cd6bc26f5fa26), [`f32d093a2`](https://github.com/withastro/astro/commit/f32d093a280faafff024228c12bb438156ec34d7), [`f01eb585e`](https://github.com/withastro/astro/commit/f01eb585e7c972d940761309b1595f682b6922d2), [`b76c166bd`](https://github.com/withastro/astro/commit/b76c166bdd8e28683f62806aef968d1e0c3b06d9), [`a87cbe400`](https://github.com/withastro/astro/commit/a87cbe400314341d5f72abf86ea264e6b47c091f), [`866ed4098`](https://github.com/withastro/astro/commit/866ed4098edffb052239cdb26e076cf8db61b1d9), [`767eb6866`](https://github.com/withastro/astro/commit/767eb68666eb777965baa0d6ade20bbafecf95bf), [`32669cd47`](https://github.com/withastro/astro/commit/32669cd47555e9c7433c3998a2b6e624dfb2d8e9)]: + - astro@3.0.0 + +## 6.0.0-rc.1 + +### Major Changes + +- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate + +### Patch Changes + +- [#8176](https://github.com/withastro/astro/pull/8176) [`d08c83ee3`](https://github.com/withastro/astro/commit/d08c83ee3fe0f10374264f61ee473255dcf0cd06) Thanks [@ematipico](https://github.com/ematipico)! - Fix an issue where `express` couldn't use the `handler` in `middleware` mode. + +- Updated dependencies [[`adf9fccfd`](https://github.com/withastro/astro/commit/adf9fccfdda107c2224558f1c2e6a77847ac0a8a), [`582132328`](https://github.com/withastro/astro/commit/5821323285646aee7ff9194a505f708028e4db57), [`81545197a`](https://github.com/withastro/astro/commit/81545197a32fd015d763fc386c8b67e0e08b7393), [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7), [`be6bbd2c8`](https://github.com/withastro/astro/commit/be6bbd2c86b9bf5268e765bb937dda00ff15781a), [`42785c7b7`](https://github.com/withastro/astro/commit/42785c7b784b151e6d582570e5d74482129e8eb8), [`95120efbe`](https://github.com/withastro/astro/commit/95120efbe817163663492181cbeb225849354493), [`2ae9d37f0`](https://github.com/withastro/astro/commit/2ae9d37f0a9cb21ab288d3c30aecb6d84db87788), [`f003e7364`](https://github.com/withastro/astro/commit/f003e7364317cafdb8589913b26b28e928dd07c9), [`732111cdc`](https://github.com/withastro/astro/commit/732111cdce441639db31f40f621df48442d00969), [`33b8910cf`](https://github.com/withastro/astro/commit/33b8910cfdce5713891c50a84a0a8fe926311710), [`e79e3779d`](https://github.com/withastro/astro/commit/e79e3779df0ad35253abcdb931d622847d9adb12), [`179796405`](https://github.com/withastro/astro/commit/179796405e053b559d83f84507e5a465861a029a), [`a87cbe400`](https://github.com/withastro/astro/commit/a87cbe400314341d5f72abf86ea264e6b47c091f), [`767eb6866`](https://github.com/withastro/astro/commit/767eb68666eb777965baa0d6ade20bbafecf95bf)]: + - astro@3.0.0-rc.5 + +## 6.0.0-beta.0 + +### Major Changes + +- [`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023. + +- [`3dc1ca2fa`](https://github.com/withastro/astro/commit/3dc1ca2fac8d9965cc5085a5d09e72ed87b4281a) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Reduced the amount of polyfills provided by Astro. Astro will no longer provide (no-op) polyfills for several web apis such as HTMLElement, Image or Document. If you need access to those APIs on the server, we recommend using more proper polyfills available on npm. + +### Minor Changes + +- [`9b4f70a62`](https://github.com/withastro/astro/commit/9b4f70a629f55e461759ba46f68af7097a2e9215) Thanks [@ematipico](https://github.com/ematipico)! - Introduced the concept of feature map. A feature map is a list of features that are built-in in Astro, and an Adapter + can tell Astro if it can support it. + + ```ts + import { AstroIntegration } from './astro'; + + function myIntegration(): AstroIntegration { + return { + name: 'astro-awesome-list', + // new feature map + supportedAstroFeatures: { + hybridOutput: 'experimental', + staticOutput: 'stable', + serverOutput: 'stable', + assets: { + supportKind: 'stable', + isSharpCompatible: false, + isSquooshCompatible: false, + }, + }, + }; + } + ``` + +### Patch Changes + +- Updated dependencies [[`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81), [`76ddef19c`](https://github.com/withastro/astro/commit/76ddef19ccab6e5f7d3a5740cd41acf10e334b38), [`9b4f70a62`](https://github.com/withastro/astro/commit/9b4f70a629f55e461759ba46f68af7097a2e9215), [`3fdf509b2`](https://github.com/withastro/astro/commit/3fdf509b2731a9b2f972d89291e57cf78d62c769), [`2f951cd40`](https://github.com/withastro/astro/commit/2f951cd403dfcc2c3ca6aae618ae3e1409516e32), [`c022a4217`](https://github.com/withastro/astro/commit/c022a4217a805d223c1494e9eda4e48bbf810388), [`67becaa58`](https://github.com/withastro/astro/commit/67becaa580b8f787df58de66b7008b7098f1209c), [`bc37331d8`](https://github.com/withastro/astro/commit/bc37331d8154e3e95a8df9131e4e014e78a7a9e7), [`dfc2d93e3`](https://github.com/withastro/astro/commit/dfc2d93e3c645995379358fabbdfa9aab99f43d8), [`3dc1ca2fa`](https://github.com/withastro/astro/commit/3dc1ca2fac8d9965cc5085a5d09e72ed87b4281a), [`1be84dfee`](https://github.com/withastro/astro/commit/1be84dfee3ce8e6f5cc624f99aec4e980f6fde37), [`35f01df79`](https://github.com/withastro/astro/commit/35f01df797d23315f2bee2fc3fd795adb0559c58), [`3fdf509b2`](https://github.com/withastro/astro/commit/3fdf509b2731a9b2f972d89291e57cf78d62c769), [`78de801f2`](https://github.com/withastro/astro/commit/78de801f21fd4ca1653950027d953bf08614566b), [`59d6e569f`](https://github.com/withastro/astro/commit/59d6e569f63e175c97e82e94aa7974febfb76f7c), [`7723c4cc9`](https://github.com/withastro/astro/commit/7723c4cc93298c2e6530e55da7afda048f22cf81), [`fb5cd6b56`](https://github.com/withastro/astro/commit/fb5cd6b56dc27a71366ed5e1ab8bfe9b8f96bac5), [`631b9c410`](https://github.com/withastro/astro/commit/631b9c410d5d66fa384674027ba95d69ebb5063f)]: + - astro@3.0.0-beta.0 + +## 5.3.6 + +### Patch Changes + +- [#8176](https://github.com/withastro/astro/pull/8176) [`d08c83ee3`](https://github.com/withastro/astro/commit/d08c83ee3fe0f10374264f61ee473255dcf0cd06) Thanks [@ematipico](https://github.com/ematipico)! - Fix an issue where `express` couldn't use the `handler` in `middleware` mode. + +- Updated dependencies [[`582132328`](https://github.com/withastro/astro/commit/5821323285646aee7ff9194a505f708028e4db57), [`fddd4dc71`](https://github.com/withastro/astro/commit/fddd4dc71af321bd6b4d01bb4b1b955284846e60), [`cfc465dde`](https://github.com/withastro/astro/commit/cfc465ddebcc58d20f29ecffaa857a77525435a9), [`95120efbe`](https://github.com/withastro/astro/commit/95120efbe817163663492181cbeb225849354493), [`273335cb0`](https://github.com/withastro/astro/commit/273335cb01615c3c06d46c02464f4496a81f8d0b), [`9142178b1`](https://github.com/withastro/astro/commit/9142178b113443749b87c1d259859b42a3d7a9c4), [`179796405`](https://github.com/withastro/astro/commit/179796405e053b559d83f84507e5a465861a029a)]: + - astro@2.10.13 + +## 5.3.5 + +### Patch Changes + +- [#8141](https://github.com/withastro/astro/pull/8141) [`4c15c0696`](https://github.com/withastro/astro/commit/4c15c069691ca25efcb9ebb7d9b45605cd136ed3) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixed an issue where the preview mode handled 404 and 500 routes differently from running app with node directly. + +- Updated dependencies [[`04caa99c4`](https://github.com/withastro/astro/commit/04caa99c48ce604ca3b90302ff0df8dcdbeee650)]: + - astro@2.10.12 + +## 5.3.4 + +### Patch Changes + +- [#8084](https://github.com/withastro/astro/pull/8084) [`560e45924`](https://github.com/withastro/astro/commit/560e45924622141206ff5b47d134cb343d6d2a71) Thanks [@hbgl](https://github.com/hbgl)! - Stream request body instead of buffering it in memory. + +- Updated dependencies [[`c19987df0`](https://github.com/withastro/astro/commit/c19987df0be3520cf774476cea270c03edd08354), [`560e45924`](https://github.com/withastro/astro/commit/560e45924622141206ff5b47d134cb343d6d2a71), [`afc45af20`](https://github.com/withastro/astro/commit/afc45af2022f7c43fbb6c5c04983695f3819e47e), [`d1f7143f9`](https://github.com/withastro/astro/commit/d1f7143f9caf2ffa0e87cc55c0e05339d3501db3), [`3e46634fd`](https://github.com/withastro/astro/commit/3e46634fd540e5b967d2e5c9abd6235452cee2f2), [`a12027b6a`](https://github.com/withastro/astro/commit/a12027b6af411be39700919ca47e240a335e9887)]: + - astro@2.10.8 + +## 5.3.3 + +### Patch Changes + +- [#6928](https://github.com/withastro/astro/pull/6928) [`b16cb787f`](https://github.com/withastro/astro/commit/b16cb787fd16ebaaf860d8bb183789caf01c0fb7) Thanks [@JerryWu1234](https://github.com/JerryWu1234)! - Support the `--host` flag when running the standalone server (also works for `astro preview --host`) + +- Updated dependencies [[`1b8d30209`](https://github.com/withastro/astro/commit/1b8d3020990130dabfaaf753db73a32c6e0c896a), [`405913cdf`](https://github.com/withastro/astro/commit/405913cdf20b26407aa351c090f0a0859a4e6f54), [`87d4b1843`](https://github.com/withastro/astro/commit/87d4b18437c7565c48cad4bea81831c2a244ebb8), [`c23377caa`](https://github.com/withastro/astro/commit/c23377caafbc75deb91c33b9678c1b6868ad40ea), [`86bee2812`](https://github.com/withastro/astro/commit/86bee2812185df6e14025e5962a335f51853587b)]: + - astro@2.10.6 + +## 5.3.2 + +### 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 + +- 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 + +## 5.3.1 + +### Patch Changes + +- [#7754](https://github.com/withastro/astro/pull/7754) [`298dbb89f`](https://github.com/withastro/astro/commit/298dbb89f2963a547370b6e65cafd2650fdb1b27) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improve `404` behavior in middleware mode + +- Updated dependencies [[`298dbb89f`](https://github.com/withastro/astro/commit/298dbb89f2963a547370b6e65cafd2650fdb1b27), [`9e2203847`](https://github.com/withastro/astro/commit/9e22038472c8be05ed7a72620534b88324dce793), [`5c5da8d2f`](https://github.com/withastro/astro/commit/5c5da8d2fbb37830f3ee81830d4c9afcd2c1a3e3), [`0b8375fe8`](https://github.com/withastro/astro/commit/0b8375fe82a15bfff3f517f98de6454adb2779f1), [`89d015db6`](https://github.com/withastro/astro/commit/89d015db6ce4d15b5b1140f0eb6bfbef187d6ad7), [`ebf7ebbf7`](https://github.com/withastro/astro/commit/ebf7ebbf7ae767625d736fad327954cfb853837e)]: + - astro@2.9.7 + +## 5.3.0 + +### Minor Changes + +- [#7385](https://github.com/withastro/astro/pull/7385) [`8e2923cc6`](https://github.com/withastro/astro/commit/8e2923cc6219eda01ca2c749f5c7fa2fe4319455) Thanks [@ematipico](https://github.com/ematipico)! - `Astro.locals` is now exposed to the adapter API. Node Adapter can now pass in a `locals` object in the SSR handler middleware. + +### Patch Changes + +- Updated dependencies [[`30bb36371`](https://github.com/withastro/astro/commit/30bb363713e3d2c50d0d4816d970aa93b836a3b0), [`3943fa390`](https://github.com/withastro/astro/commit/3943fa390a0bd41317a673d0f841e0461c7499cd), [`7877a06d8`](https://github.com/withastro/astro/commit/7877a06d829305eed356fbb8bfd1ef578cd5466e), [`e314a04bf`](https://github.com/withastro/astro/commit/e314a04bfbf0526838b7c9aac452251b27d69719), [`33cdc8622`](https://github.com/withastro/astro/commit/33cdc8622a56c8e5465b7a50f627ecc568870c6b), [`76fcdb84d`](https://github.com/withastro/astro/commit/76fcdb84dd828ac373b2dc739e57fadf650820fd), [`8e2923cc6`](https://github.com/withastro/astro/commit/8e2923cc6219eda01ca2c749f5c7fa2fe4319455), [`459b5bd05`](https://github.com/withastro/astro/commit/459b5bd05f562238f7250520efe3cf0fa156bb45)]: + - astro@2.7.0 + +## 5.2.0 + +### Minor Changes + +- [#7227](https://github.com/withastro/astro/pull/7227) [`4929332c3`](https://github.com/withastro/astro/commit/4929332c3210d1634b8607c7736d9049860a2079) Thanks [@alex-sherwin](https://github.com/alex-sherwin)! - Fixes NodeJS adapter for multiple set-cookie headers and combining AstroCookies and Response.headers cookies + +### Patch Changes + +- [#7243](https://github.com/withastro/astro/pull/7243) [`409c60028`](https://github.com/withastro/astro/commit/409c60028aaab09b8f2383ef5730531cd23db4ba) Thanks [@Riki-WangJJ](https://github.com/Riki-WangJJ)! - Support directory redirects and query params at the same time + +- [#7260](https://github.com/withastro/astro/pull/7260) [`39403c32f`](https://github.com/withastro/astro/commit/39403c32faea58399c61d3344b770f195be60d5b) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Unflags support for `output: 'hybrid'` mode, which enables pre-rendering by default. The additional `experimental.hybridOutput` flag can be safely removed from your configuration. + +- Updated dependencies [[`57f8d14c0`](https://github.com/withastro/astro/commit/57f8d14c027c30919363e12c664ccff4ed64d0fc), [`414eb19d2`](https://github.com/withastro/astro/commit/414eb19d2fcb55758f9d053076773b11b62f4c97), [`a7e2b37ff`](https://github.com/withastro/astro/commit/a7e2b37ff73871c46895c615846a86a539f45330), [`dd1a6b6c9`](https://github.com/withastro/astro/commit/dd1a6b6c941aeb7af934bd12db22412af262f5a1), [`d72cfa7ca`](https://github.com/withastro/astro/commit/d72cfa7cad758192163712ceb269405659fd14bc), [`144813f73`](https://github.com/withastro/astro/commit/144813f7308dcb9de64ebe3f0f2c6cba9ad81eb1), [`b5213654b`](https://github.com/withastro/astro/commit/b5213654b1b7f3ba573a48d3be688b2bdde7870f), [`e3b8c6296`](https://github.com/withastro/astro/commit/e3b8c62969d680d1915a122c610d281d6711aa63), [`890a2bc98`](https://github.com/withastro/astro/commit/890a2bc9891a2449ab99b01b65468f6dddba6b12), [`39403c32f`](https://github.com/withastro/astro/commit/39403c32faea58399c61d3344b770f195be60d5b), [`101f03209`](https://github.com/withastro/astro/commit/101f032098148b3daaac8d46ff1e535b79232e43)]: + - astro@2.6.0 + +## 5.1.4 + +### Patch Changes + +- [#6991](https://github.com/withastro/astro/pull/6991) [`719002ca5`](https://github.com/withastro/astro/commit/719002ca5b128744fb4316d4a52c5dcd46a42759) Thanks [@MoustaphaDev](https://github.com/MoustaphaDev)! - Enable experimental support for hybrid SSR with pre-rendering enabled by default + + **astro.config.mjs** + + ```js + import { defineConfig } from 'astro/config'; + export default defineConfig({ + output: 'hybrid', + experimental: { + hybridOutput: true, + }, + }); + ``` + + Then add `export const prerender = false` to any page or endpoint you want to opt-out of pre-rendering. + + **src/pages/contact.astro** + + ```astro + --- + export const prerender = false; + + if (Astro.request.method === 'POST') { + // handle form submission + } + --- + + <form method="POST"> + <input type="text" name="name" /> + <input type="email" name="email" /> + <button type="submit">Submit</button> + </form> + ``` + +- [#7104](https://github.com/withastro/astro/pull/7104) [`826e02890`](https://github.com/withastro/astro/commit/826e0289005f645b902375b98d5549c6a95ccafa) Thanks [@bluwy](https://github.com/bluwy)! - Specify `"files"` field to only publish necessary files + +- Updated dependencies [[`4516d7b22`](https://github.com/withastro/astro/commit/4516d7b22c5979cde4537f196b53ae2826ba9561), [`e186ecc5e`](https://github.com/withastro/astro/commit/e186ecc5e292de8c6a2c441a2d588512c0813068), [`c6d7ebefd`](https://github.com/withastro/astro/commit/c6d7ebefdd554a9ef29cfeb426ac55cab80d6473), [`914c439bc`](https://github.com/withastro/astro/commit/914c439bccee9fec002c6d92beaa501c398e62ac), [`e9fc2c221`](https://github.com/withastro/astro/commit/e9fc2c2213036d47cd30a47a6cdad5633481a0f8), [`075eee08f`](https://github.com/withastro/astro/commit/075eee08f2e2b0baea008b97f3523f2cb937ee44), [`719002ca5`](https://github.com/withastro/astro/commit/719002ca5b128744fb4316d4a52c5dcd46a42759), [`fc52681ba`](https://github.com/withastro/astro/commit/fc52681ba2f8fe8bcd92eeedf3c6a52fd86a390e), [`fb84622af`](https://github.com/withastro/astro/commit/fb84622af04f795de8d17f24192de105f70fe910), [`cada10a46`](https://github.com/withastro/astro/commit/cada10a466f81f8edb0aa664f9cffdb6b5b8f307), [`cd410c5eb`](https://github.com/withastro/astro/commit/cd410c5eb71f825259279c27c4c39d0ad282c3f0), [`73ec6f6c1`](https://github.com/withastro/astro/commit/73ec6f6c16cadb71dafe9f664f0debde072c3173), [`410428672`](https://github.com/withastro/astro/commit/410428672ed97bba7ca0b3352c1a7ee564921462), [`763ff2d1e`](https://github.com/withastro/astro/commit/763ff2d1e44f54b899d7c65386f1b4b877c95737), [`c1669c001`](https://github.com/withastro/astro/commit/c1669c0011eecfe65a459d727848c18c189a54ca), [`3d525efc9`](https://github.com/withastro/astro/commit/3d525efc95cfb2deb5d9e04856d02965d66901c9)]: + - astro@2.5.0 + +## 5.1.3 + +### Patch Changes + +- [#7076](https://github.com/withastro/astro/pull/7076) [`781f558c4`](https://github.com/withastro/astro/commit/781f558c401a5f02927d150e4628a77c55cccd28) Thanks [@matthewp](https://github.com/matthewp)! - Fix redirects on directories when using base option + +## 5.1.2 + +### Patch Changes + +- [#6935](https://github.com/withastro/astro/pull/6935) [`c405cef64`](https://github.com/withastro/astro/commit/c405cef64711a7b6a480e8b4068cd2bf3cf889a9) Thanks [@matthewp](https://github.com/matthewp)! - Catch errors that occur within the stream in the Node adapter + +- Updated dependencies [[`a98df9374`](https://github.com/withastro/astro/commit/a98df9374dec65c678fa47319cb1481b1af123e2), [`ac57b5549`](https://github.com/withastro/astro/commit/ac57b5549f828a17bdbebdaca7ace075307a3c9d), [`50975f2ea`](https://github.com/withastro/astro/commit/50975f2ea3a59f9e023cc631a9372c0c7986eec9), [`ebae1eaf8`](https://github.com/withastro/astro/commit/ebae1eaf87f49399036033c673b513338f7d9c42), [`dc062f669`](https://github.com/withastro/astro/commit/dc062f6695ce577dc569781fc0678c903012c336)]: + - astro@2.3.3 + - @astrojs/webapi@2.1.1 + +## 5.1.1 + +### Patch Changes + +- [#6746](https://github.com/withastro/astro/pull/6746) [`4cc1bf61b`](https://github.com/withastro/astro/commit/4cc1bf61b832dba9aab1916b56f5260ceac2d97d) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix malformed URLs crashing the server in certain cases + +- Updated dependencies [[`489dd8d69`](https://github.com/withastro/astro/commit/489dd8d69cdd9d7c243cf8bec96051a914984b9c), [`a1a4f45b5`](https://github.com/withastro/astro/commit/a1a4f45b51a80215fa7598da83bd0d9c5acd20d2), [`a1108e037`](https://github.com/withastro/astro/commit/a1108e037115cdb67d03505286c7d3a4fc2a1ff5), [`8b88e4cf1`](https://github.com/withastro/astro/commit/8b88e4cf15c8bea7942b3985380164e0edf7250b), [`d54cbe413`](https://github.com/withastro/astro/commit/d54cbe41349e55f8544212ad9320705f07325920), [`4c347ab51`](https://github.com/withastro/astro/commit/4c347ab51e46f2319d614f8577fe502e3dc816e2), [`ff0430786`](https://github.com/withastro/astro/commit/ff043078630e678348ae4f4757b3015b3b862c16), [`2f2e572e9`](https://github.com/withastro/astro/commit/2f2e572e937fd25451bbc78a05d55b7caa1ca3ec), [`7116c021a`](https://github.com/withastro/astro/commit/7116c021a39eac15a6e1264dfbd11bef0f5d618a)]: + - astro@2.2.0 + +## 5.1.0 + +### Minor Changes + +- [#6213](https://github.com/withastro/astro/pull/6213) [`afbbc4d5b`](https://github.com/withastro/astro/commit/afbbc4d5bfafc1779bac00b41c2a1cb1c90f2808) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updated compilation settings to disable downlevelling for Node 14 + +### Patch Changes + +- Updated dependencies [[`fec583909`](https://github.com/withastro/astro/commit/fec583909ab62829dc0c1600e2387979365f2b94), [`b087b83fe`](https://github.com/withastro/astro/commit/b087b83fe266c431fe34a07d5c2293cc4ab011c6), [`694918a56`](https://github.com/withastro/astro/commit/694918a56b01104831296be0c25456135a63c784), [`a20610609`](https://github.com/withastro/astro/commit/a20610609863ae3b48afe96819b8f11ae4f414d5), [`a4a74ab70`](https://github.com/withastro/astro/commit/a4a74ab70cd2aa0d812a1f6b202c4e240a8913bf), [`75921b3cd`](https://github.com/withastro/astro/commit/75921b3cd916d439f6392c487c21532fde35ed13), [`afbbc4d5b`](https://github.com/withastro/astro/commit/afbbc4d5bfafc1779bac00b41c2a1cb1c90f2808)]: + - astro@2.1.0 + - @astrojs/webapi@2.1.0 + +## 5.0.4 + +### Patch Changes + +- [#6323](https://github.com/withastro/astro/pull/6323) [`5e26bc891`](https://github.com/withastro/astro/commit/5e26bc891cbebb3598acfa760c135a25c548d624) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updated Undici to 5.20.0. This fixes a security issue and handling of cookies in certain cases in dev + +- Updated dependencies [[`5e26bc891`](https://github.com/withastro/astro/commit/5e26bc891cbebb3598acfa760c135a25c548d624), [`a156ecbb7`](https://github.com/withastro/astro/commit/a156ecbb7f4df6a46124a9a12eb712f9163db2ed), [`ccd72e6bb`](https://github.com/withastro/astro/commit/ccd72e6bb41e570d42b1b158e8124c8e04a1943d), [`504c7bacb`](https://github.com/withastro/astro/commit/504c7bacb8c1f2308a31e6c412825ba34983ba33), [`63dda6ded`](https://github.com/withastro/astro/commit/63dda6dedd4c6ea1d5ce72e9cf3fe5f88339a927), [`f91a7f376`](https://github.com/withastro/astro/commit/f91a7f376c223f18b4d8fbed81f95f6bea1cef8d)]: + - astro@2.0.15 + +## 5.0.3 + +### Patch Changes + +- [#6110](https://github.com/withastro/astro/pull/6110) [`67ccec9e1`](https://github.com/withastro/astro/commit/67ccec9e168f241318d9dac40096016982d89b7b) Thanks [@matthewp](https://github.com/matthewp)! - Fixes support for prerendering and query params + +## 5.0.2 + +### 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 + +## 5.0.1 + +### Patch Changes + +- [#5992](https://github.com/withastro/astro/pull/5992) [`60b32d585`](https://github.com/withastro/astro/commit/60b32d58565d87e87573eb268408293fc28ec657) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fix `Astro.url.protocol` when using the @astrojs/node SSR adapter with HTTPS + +- Updated dependencies [[`b53e0717b`](https://github.com/withastro/astro/commit/b53e0717b7f6b042baaeec7f87999e99c76c031c), [`60b32d585`](https://github.com/withastro/astro/commit/60b32d58565d87e87573eb268408293fc28ec657), [`883e0cc29`](https://github.com/withastro/astro/commit/883e0cc29968d51ed6c7515be035a40b28bafdad), [`dabce6b8c`](https://github.com/withastro/astro/commit/dabce6b8c684f851c3535f8acead06cbef6dce2a), [`aedf23f85`](https://github.com/withastro/astro/commit/aedf23f8582e32a6b94b81ddba9b323831f2b22a)]: + - astro@2.0.2 + +## 5.0.0 + +### Major Changes + +- [#5782](https://github.com/withastro/astro/pull/5782) [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 14. Minimum supported Node version is now >=16.12.0 + +- [#5707](https://github.com/withastro/astro/pull/5707) [`5eba34fcc`](https://github.com/withastro/astro/commit/5eba34fcc663def20bdf6e0daad02a6a5472776b) Thanks [@bluwy](https://github.com/bluwy)! - Remove `astro:build:start` backwards compatibility code + +- [#5806](https://github.com/withastro/astro/pull/5806) [`7572f7402`](https://github.com/withastro/astro/commit/7572f7402238da37de748be58d678fedaf863b53) Thanks [@matthewp](https://github.com/matthewp)! - Make astro a `peerDependency` of integrations + + This marks `astro` as a `peerDependency` of several packages that are already getting `major` version bumps. This is so we can more properly track the dependency between them and what version of Astro they are being used with. + +### Minor Changes + +- [#5832](https://github.com/withastro/astro/pull/5832) [`2303f9514`](https://github.com/withastro/astro/commit/2303f95142aa740c99213a098f82b99dd37d74a0) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Add support for serving well-known URIs with the @astrojs/node SSR adapter + +### Patch Changes + +- [#5701](https://github.com/withastro/astro/pull/5701) [`9869f2f6d`](https://github.com/withastro/astro/commit/9869f2f6d8c344babb8a59cb54918de14bd95dcc) Thanks [@wulinsheng123](https://github.com/wulinsheng123)! - Support custom 404 page in standalone mode + +- Updated dependencies [[`93e633922`](https://github.com/withastro/astro/commit/93e633922c2e449df3bb2357b3683af1d3c0e07b), [`16dc36a87`](https://github.com/withastro/astro/commit/16dc36a870df47a4151a8ed2d91d0bd1bb812458), [`01f3f463b`](https://github.com/withastro/astro/commit/01f3f463bf2918b310d130a9fabbf3ee21d14029), [`e2019be6f`](https://github.com/withastro/astro/commit/e2019be6ffa46fa33d92cfd346f9ecbe51bb7144), [`05caf445d`](https://github.com/withastro/astro/commit/05caf445d4d2728f1010aeb2179a9e756c2fd17d), [`49ab4f231`](https://github.com/withastro/astro/commit/49ab4f231c23b34891c3ee86f4b92bf8d6d267a3), [`a342a486c`](https://github.com/withastro/astro/commit/a342a486c2831461e24e6c2f1ca8a9d3e15477b6), [`8fb28648f`](https://github.com/withastro/astro/commit/8fb28648f66629741cb976bfe34ccd9d8f55661e), [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a), [`c2180746b`](https://github.com/withastro/astro/commit/c2180746b4f6d9ef1b6f86924f21f52cc6ab4e63), [`ae8a012a7`](https://github.com/withastro/astro/commit/ae8a012a7b6884a03c50494332ee37b4505c2c3b), [`cf2de5422`](https://github.com/withastro/astro/commit/cf2de5422c26bfdea4c75f76e57b57299ded3e3a), [`ce5c5dbd4`](https://github.com/withastro/astro/commit/ce5c5dbd46afbe738b03600758bf5c35113de522), [`ec09bb664`](https://github.com/withastro/astro/commit/ec09bb6642064dbd7d2f3369afb090363ae18de2), [`665a2c222`](https://github.com/withastro/astro/commit/665a2c2225e42881f5a9550599e8f3fc1deea0b4), [`259a539d7`](https://github.com/withastro/astro/commit/259a539d7d70c783330c797794b15716921629cf), [`f7aa1ec25`](https://github.com/withastro/astro/commit/f7aa1ec25d1584f7abd421903fbef66b1c050e2a), [`4987d6f44`](https://github.com/withastro/astro/commit/4987d6f44cfd0d81d88f21f5c380503403dc1e6a), [`304823811`](https://github.com/withastro/astro/commit/304823811eddd8e72aa1d8e2d39b40ab5cda3565), [`302e0ef8f`](https://github.com/withastro/astro/commit/302e0ef8f5d5232e3348afe680e599f3e537b5c5), [`55cea0a9d`](https://github.com/withastro/astro/commit/55cea0a9d8c8df91a46590fc04a9ac28089b3432), [`dd56c1941`](https://github.com/withastro/astro/commit/dd56c19411b126439b8bc42d681b6fa8c06e8c61), [`9963c6e4d`](https://github.com/withastro/astro/commit/9963c6e4d50c392c3d1ac4492237020f15ccb1de), [`46ecd5de3`](https://github.com/withastro/astro/commit/46ecd5de34df619e2ee73ccea39a57acd37bc0b8), [`be901dc98`](https://github.com/withastro/astro/commit/be901dc98c4a7f6b5536540aa8f7ba5108e939a0), [`f6cf92b48`](https://github.com/withastro/astro/commit/f6cf92b48317a19a3840ad781b77d6d3cae143bb), [`e818cc046`](https://github.com/withastro/astro/commit/e818cc0466a942919ea3c41585e231c8c80cb3d0), [`8c100a6fe`](https://github.com/withastro/astro/commit/8c100a6fe6cc652c3799d1622e12c2c969f30510), [`116d8835c`](https://github.com/withastro/astro/commit/116d8835ca9e78f8b5e477ee5a3d737b69f80706), [`840412128`](https://github.com/withastro/astro/commit/840412128b00a04515156e92c314a929d6b94f6d), [`1f49cddf9`](https://github.com/withastro/astro/commit/1f49cddf9e9ffc651efc171b2cbde9fbe9e8709d), [`7325df412`](https://github.com/withastro/astro/commit/7325df412107fc0e65cd45c1b568fb686708f723), [`16c7d0bfd`](https://github.com/withastro/astro/commit/16c7d0bfd49d2b9bfae45385f506bcd642f9444a), [`c55fbcb8e`](https://github.com/withastro/astro/commit/c55fbcb8edca1fe118a44f68c9f9436a4719d171), [`a9c292026`](https://github.com/withastro/astro/commit/a9c2920264e36cc5dc05f4adc1912187979edb0d), [`2a5786419`](https://github.com/withastro/astro/commit/2a5786419599b8674473c699300172b9aacbae2e), [`4a1cabfe6`](https://github.com/withastro/astro/commit/4a1cabfe6b9ef8a6fbbcc0727a0dc6fa300cedaa), [`a8d3e7924`](https://github.com/withastro/astro/commit/a8d3e79246605d252dcddad159e358e2d79bd624), [`fa8c131f8`](https://github.com/withastro/astro/commit/fa8c131f88ef67d14c62f1c00c97ed74d43a80ac), [`64b8082e7`](https://github.com/withastro/astro/commit/64b8082e776b832f1433ed288e6f7888adb626d0), [`c4b0cb8bf`](https://github.com/withastro/astro/commit/c4b0cb8bf2b41887d9106440bb2e70d421a5f481), [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a), [`23dc9ea96`](https://github.com/withastro/astro/commit/23dc9ea96a10343852d965efd41fe6665294f1fb), [`63a6ceb38`](https://github.com/withastro/astro/commit/63a6ceb38d88331451dca64d0034c7c58e3d26f1), [`a3a7fc929`](https://github.com/withastro/astro/commit/a3a7fc9298e6d88abb4b7bee1e58f05fa9558cf1), [`52209ca2a`](https://github.com/withastro/astro/commit/52209ca2ad72a30854947dcb3a90ab4db0ac0a6f), [`5fd9208d4`](https://github.com/withastro/astro/commit/5fd9208d447f5ab8909a2188b6c2491a0debd49d), [`5eba34fcc`](https://github.com/withastro/astro/commit/5eba34fcc663def20bdf6e0daad02a6a5472776b), [`899214298`](https://github.com/withastro/astro/commit/899214298cee5f0c975c7245e623c649e1842d73), [`3a00ecb3e`](https://github.com/withastro/astro/commit/3a00ecb3eb4bc44be758c064f2bde6e247e8a593), [`5eba34fcc`](https://github.com/withastro/astro/commit/5eba34fcc663def20bdf6e0daad02a6a5472776b), [`2303f9514`](https://github.com/withastro/astro/commit/2303f95142aa740c99213a098f82b99dd37d74a0), [`1ca81c16b`](https://github.com/withastro/astro/commit/1ca81c16b8b66236e092e6eb6ec3f73f5668421c), [`b66d7195c`](https://github.com/withastro/astro/commit/b66d7195c17a55ea0931bc3744888bd4f5f01ce6)]: + - astro@2.0.0 + - @astrojs/webapi@2.0.0 + +## 5.0.0-beta.1 + +<details> +<summary>See changes in 5.0.0-beta.1</summary> + +### Major Changes + +- [#5782](https://github.com/withastro/astro/pull/5782) [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 14. Minimum supported Node version is now >=16.12.0 + +- [#5806](https://github.com/withastro/astro/pull/5806) [`7572f7402`](https://github.com/withastro/astro/commit/7572f7402238da37de748be58d678fedaf863b53) Thanks [@matthewp](https://github.com/matthewp)! - Make astro a `peerDependency` of integrations + + This marks `astro` as a `peerDependency` of several packages that are already getting `major` version bumps. This is so we can more properly track the dependency between them and what version of Astro they are being used with. + +### Minor Changes + +- [#5832](https://github.com/withastro/astro/pull/5832) [`2303f9514`](https://github.com/withastro/astro/commit/2303f95142aa740c99213a098f82b99dd37d74a0) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Add support for serving well-known URIs with the @astrojs/node SSR adapter + +### Patch Changes + +- [#5701](https://github.com/withastro/astro/pull/5701) [`9869f2f6d`](https://github.com/withastro/astro/commit/9869f2f6d8c344babb8a59cb54918de14bd95dcc) Thanks [@wulinsheng123](https://github.com/wulinsheng123)! - Support custom 404 page in standalone mode + +- Updated dependencies [[`01f3f463b`](https://github.com/withastro/astro/commit/01f3f463bf2918b310d130a9fabbf3ee21d14029), [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a), [`c2180746b`](https://github.com/withastro/astro/commit/c2180746b4f6d9ef1b6f86924f21f52cc6ab4e63), [`ae8a012a7`](https://github.com/withastro/astro/commit/ae8a012a7b6884a03c50494332ee37b4505c2c3b), [`cf2de5422`](https://github.com/withastro/astro/commit/cf2de5422c26bfdea4c75f76e57b57299ded3e3a), [`ec09bb664`](https://github.com/withastro/astro/commit/ec09bb6642064dbd7d2f3369afb090363ae18de2), [`665a2c222`](https://github.com/withastro/astro/commit/665a2c2225e42881f5a9550599e8f3fc1deea0b4), [`f7aa1ec25`](https://github.com/withastro/astro/commit/f7aa1ec25d1584f7abd421903fbef66b1c050e2a), [`302e0ef8f`](https://github.com/withastro/astro/commit/302e0ef8f5d5232e3348afe680e599f3e537b5c5), [`840412128`](https://github.com/withastro/astro/commit/840412128b00a04515156e92c314a929d6b94f6d), [`1f49cddf9`](https://github.com/withastro/astro/commit/1f49cddf9e9ffc651efc171b2cbde9fbe9e8709d), [`c55fbcb8e`](https://github.com/withastro/astro/commit/c55fbcb8edca1fe118a44f68c9f9436a4719d171), [`4a1cabfe6`](https://github.com/withastro/astro/commit/4a1cabfe6b9ef8a6fbbcc0727a0dc6fa300cedaa), [`c4b0cb8bf`](https://github.com/withastro/astro/commit/c4b0cb8bf2b41887d9106440bb2e70d421a5f481), [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a), [`23dc9ea96`](https://github.com/withastro/astro/commit/23dc9ea96a10343852d965efd41fe6665294f1fb), [`63a6ceb38`](https://github.com/withastro/astro/commit/63a6ceb38d88331451dca64d0034c7c58e3d26f1), [`52209ca2a`](https://github.com/withastro/astro/commit/52209ca2ad72a30854947dcb3a90ab4db0ac0a6f), [`2303f9514`](https://github.com/withastro/astro/commit/2303f95142aa740c99213a098f82b99dd37d74a0)]: + - astro@2.0.0-beta.2 + - @astrojs/webapi@2.0.0-beta.0 + +</details> + +## 5.0.0-beta.0 + +<details> +<summary>See changes in 5.0.0-beta.0</summary> + +### Major Changes + +- [#5707](https://github.com/withastro/astro/pull/5707) [`5eba34fcc`](https://github.com/withastro/astro/commit/5eba34fcc663def20bdf6e0daad02a6a5472776b) Thanks [@bluwy](https://github.com/bluwy)! - Remove `astro:build:start` backwards compatibility code + +### Patch Changes + +- Updated dependencies [[`e2019be6f`](https://github.com/withastro/astro/commit/e2019be6ffa46fa33d92cfd346f9ecbe51bb7144), [`8fb28648f`](https://github.com/withastro/astro/commit/8fb28648f66629741cb976bfe34ccd9d8f55661e), [`dd56c1941`](https://github.com/withastro/astro/commit/dd56c19411b126439b8bc42d681b6fa8c06e8c61), [`f6cf92b48`](https://github.com/withastro/astro/commit/f6cf92b48317a19a3840ad781b77d6d3cae143bb), [`16c7d0bfd`](https://github.com/withastro/astro/commit/16c7d0bfd49d2b9bfae45385f506bcd642f9444a), [`a9c292026`](https://github.com/withastro/astro/commit/a9c2920264e36cc5dc05f4adc1912187979edb0d), [`5eba34fcc`](https://github.com/withastro/astro/commit/5eba34fcc663def20bdf6e0daad02a6a5472776b), [`5eba34fcc`](https://github.com/withastro/astro/commit/5eba34fcc663def20bdf6e0daad02a6a5472776b)]: + - astro@2.0.0-beta.0 + +</details> + +## 4.0.0 + +### Patch Changes + +- Updated dependencies [[`d85ec7484`](https://github.com/withastro/astro/commit/d85ec7484ce14a4c7d3f480da8f38fcb9aff388f), [`d2960984c`](https://github.com/withastro/astro/commit/d2960984c59af7b60a3ea472c6c58fb00534a8e6), [`31ec84797`](https://github.com/withastro/astro/commit/31ec8479721a1cd65538ec041458c5ffe8f50ee9), [`5ec0f6ed5`](https://github.com/withastro/astro/commit/5ec0f6ed55b0a14a9663a90a03428345baf126bd), [`dced4a8a2`](https://github.com/withastro/astro/commit/dced4a8a2657887ec569860d9862d20f695dc23a), [`6b156dd3b`](https://github.com/withastro/astro/commit/6b156dd3b467884839a571c53114aadf26fa4b0b)]: + - astro@1.7.0 + +## 3.1.1 + +### Patch Changes + +- [#5560](https://github.com/withastro/astro/pull/5560) [`281ea9fc3`](https://github.com/withastro/astro/commit/281ea9fc344dec4348e398696e671f833334045b) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improve error message when serverEntrypoint does not exist + +- Updated dependencies [[`b2f0210c4`](https://github.com/withastro/astro/commit/b2f0210c400a547d3067fdae6d15663b827be3a6), [`02bb0a1cc`](https://github.com/withastro/astro/commit/02bb0a1ccd53e38157eec3a750160731fce64b9c), [`2bd23e454`](https://github.com/withastro/astro/commit/2bd23e454fc9559aa00b9a493772acd69ba9ce6c)]: + - astro@1.6.15 + +## 3.1.0 + +### Minor Changes + +- [#5418](https://github.com/withastro/astro/pull/5418) [`aa16b6ceb`](https://github.com/withastro/astro/commit/aa16b6cebc08e0a10a17024d31ee7d2319258a34) Thanks [@jbanety](https://github.com/jbanety)! - Sometimes Astro sends a ReadableStream as a response and it raise an error **TypeError: body is not async iterable.** + + I added a function to get a response iterator from different response types (sourced from apollo-client). + + With this, node adapter can handle all the Astro response types. + +- [#5421](https://github.com/withastro/astro/pull/5421) [`12236dbc0`](https://github.com/withastro/astro/commit/12236dbc06e1e43618b61d180020a67cb31499f8) Thanks [@Scttpr](https://github.com/Scttpr)! - Allow HOST env variable to be provided at runtime + +### Patch Changes + +- Updated dependencies [[`1ab505855`](https://github.com/withastro/astro/commit/1ab505855f9942659e3d23cb1ac668f04b98889d), [`ff35b4759`](https://github.com/withastro/astro/commit/ff35b4759bd0fecfee6c99bf510c2e32d2574992), [`b22ba1c03`](https://github.com/withastro/astro/commit/b22ba1c03a3e384dad569feb38fa34ecf7ec3b93), [`a9f7ff966`](https://github.com/withastro/astro/commit/a9f7ff96676a40b78e22379edc8eb9ce60a29fb8)]: + - astro@1.6.10 + +## 3.0.0 + +### Major Changes + +- [#5290](https://github.com/withastro/astro/pull/5290) [`b2b291d29`](https://github.com/withastro/astro/commit/b2b291d29143703cece0d12c8e74b2e1151d2061) Thanks [@matthewp](https://github.com/matthewp)! - Handle base configuration in adapters + + This allows adapters to correctly handle `base` configuration. Internally Astro now matches routes when the URL includes the `base`. + + Adapters now also have access to the `removeBase` method which will remove the `base` from a pathname. This is useful to look up files for static assets. + +### Patch Changes + +- Updated dependencies [[`b2b291d29`](https://github.com/withastro/astro/commit/b2b291d29143703cece0d12c8e74b2e1151d2061), [`97e2b6ad7`](https://github.com/withastro/astro/commit/97e2b6ad7a6fa23e82be28b2f57cdf3f85fab112), [`4af4d8fa0`](https://github.com/withastro/astro/commit/4af4d8fa0035130fbf31c82d72777c3679bc1ca5), [`f6add3924`](https://github.com/withastro/astro/commit/f6add3924d5cd59925a6ea4bf7f2f731709bc893), [`247eb7411`](https://github.com/withastro/astro/commit/247eb7411f429317e5cd7d401a6660ee73641313)]: + - astro@1.6.4 + +## 2.0.2 + +### Patch Changes + +- [#5207](https://github.com/withastro/astro/pull/5207) [`c203a5cc2`](https://github.com/withastro/astro/commit/c203a5cc2f12d8c1c3e96d4f08bdd2bb2823e997) Thanks [@BeanWei](https://github.com/BeanWei)! - fix static server path for windows system + +## 2.0.1 + +### Patch Changes + +- [#5114](https://github.com/withastro/astro/pull/5114) [`5c0c6e1ac`](https://github.com/withastro/astro/commit/5c0c6e1ac67e6341625f028794986700197334ae) Thanks [@matthewp](https://github.com/matthewp)! - Fixes finding the client folder for serving assets + +- [#5111](https://github.com/withastro/astro/pull/5111) [`df4d84610`](https://github.com/withastro/astro/commit/df4d84610ad2b543a37cb3bcac9887bfef0b8994) Thanks [@rishi-raj-jain](https://github.com/rishi-raj-jain)! - fix port in standalone mode + +## 2.0.0 + +### Major Changes + +- [#5056](https://github.com/withastro/astro/pull/5056) [`e55af8a23`](https://github.com/withastro/astro/commit/e55af8a23233b6335f45b7a04b9d026990fb616c) Thanks [@matthewp](https://github.com/matthewp)! - # Standalone mode for the Node.js adapter + + New in `@astrojs/node` is support for **standalone mode**. With standalone mode you can start your production server without needing to write any server JavaScript yourself. The server starts simply by running the script like so: + + ```shell + node ./dist/server/entry.mjs + ``` + + To enable standalone mode, set the new `mode` to `'standalone'` option in your Astro config: + + ```js + import { defineConfig } from 'astro/config'; + import nodejs from '@astrojs/node'; + + export default defineConfig({ + output: 'server', + adapter: nodejs({ + mode: 'standalone', + }), + }); + ``` + + See the @astrojs/node documentation to learn all of the options available in standalone mode. + + ## Breaking change + + This is a semver major change because the new `mode` option is required. Existing @astrojs/node users who are using their own HTTP server framework such as Express can upgrade by setting the `mode` option to `'middleware'` in order to build to a middleware mode, which is the same behavior and API as before. + + ```js + import { defineConfig } from 'astro/config'; + import nodejs from '@astrojs/node'; + + export default defineConfig({ + output: 'server', + adapter: nodejs({ + mode: 'middleware', + }), + }); + ``` + +### Minor Changes + +- [#5056](https://github.com/withastro/astro/pull/5056) [`e55af8a23`](https://github.com/withastro/astro/commit/e55af8a23233b6335f45b7a04b9d026990fb616c) Thanks [@matthewp](https://github.com/matthewp)! - # Adapter support for `astro preview` + + Adapters are now about to support the `astro preview` command via a new integration option. The Node.js adapter `@astrojs/node` is the first of the built-in adapters to gain support for this. What this means is that if you are using `@astrojs/node` you can new preview your SSR app by running: + + ```shell + npm run preview + ``` + + ## Adapter API + + We will be updating the other first party Astro adapters to support preview over time. Adapters can opt in to this feature by providing the `previewEntrypoint` via the `setAdapter` function in `astro:config:done` hook. The Node.js adapter's code looks like this: + + ```diff + export default function() { + return { + name: '@astrojs/node', + hooks: { + 'astro:config:done': ({ setAdapter, config }) => { + setAdapter({ + name: '@astrojs/node', + serverEntrypoint: '@astrojs/node/server.js', + + previewEntrypoint: '@astrojs/node/preview.js', + exports: ['handler'], + }); + + // more here + } + } + }; + } + ``` + + The `previewEntrypoint` is a module in the adapter's package that is a Node.js script. This script is run when `astro preview` is run and is charged with starting up the built server. See the Node.js implementation in `@astrojs/node` to see how that is implemented. + +- [#5056](https://github.com/withastro/astro/pull/5056) [`e55af8a23`](https://github.com/withastro/astro/commit/e55af8a23233b6335f45b7a04b9d026990fb616c) Thanks [@matthewp](https://github.com/matthewp)! - # New build configuration + + The ability to customize SSR build configuration more granularly is now available in Astro. You can now customize the output folder for `server` (the server code for SSR), `client` (your client-side JavaScript and assets), and `serverEntry` (the name of the entrypoint server module). Here are the defaults: + + ```js + import { defineConfig } from 'astro/config'; + + export default defineConfig({ + output: 'server', + build: { + server: './dist/server/', + client: './dist/client/', + serverEntry: 'entry.mjs', + }, + }); + ``` + + These new configuration options are only supported in SSR mode and are ignored when building to SSG (a static site). + + ## Integration hook change + + The integration hook `astro:build:start` includes a param `buildConfig` which includes all of these same options. You can continue to use this param in Astro 1.x, but it is deprecated in favor of the new `build.config` options. All of the built-in adapters have been updated to the new format. If you have an integration that depends on this param we suggest upgrading to do this instead: + + ```js + export default function myIntegration() { + return { + name: 'my-integration', + hooks: { + 'astro:config:setup': ({ updateConfig }) => { + updateConfig({ + build: { + server: '...', + }, + }); + }, + }, + }; + } + ``` + +## 1.1.0 + +### Minor Changes + +- [#4876](https://github.com/withastro/astro/pull/4876) [`d3091f89e`](https://github.com/withastro/astro/commit/d3091f89e92fcfe1ad48daca74055d54b1c853a3) Thanks [@matthewp](https://github.com/matthewp)! - Adds the Astro.cookies API + + `Astro.cookies` is a new API for manipulating cookies in Astro components and API routes. + + In Astro components, the new `Astro.cookies` object is a map-like object that allows you to get, set, delete, and check for a cookie's existence (`has`): + + ```astro + --- + type Prefs = { + darkMode: boolean; + }; + + Astro.cookies.set<Prefs>( + 'prefs', + { darkMode: true }, + { + expires: '1 month', + } + ); + + const prefs = Astro.cookies.get<Prefs>('prefs').json(); + --- + + <body data-theme={prefs.darkMode ? 'dark' : 'light'}></body> + ``` + + Once you've set a cookie with Astro.cookies it will automatically be included in the outgoing response. + + This API is also available with the same functionality in API routes: + + ```js + export function post({ cookies }) { + cookies.set('loggedIn', false); + + return new Response(null, { + status: 302, + headers: { + Location: '/login', + }, + }); + } + ``` + + See [the RFC](https://github.com/withastro/rfcs/blob/main/proposals/0025-cookie-management.md) to learn more. + +## 1.0.1 + +### Patch Changes + +- [#4558](https://github.com/withastro/astro/pull/4558) [`742966456`](https://github.com/withastro/astro/commit/7429664566f05ecebf6d57906f950627e62e690c) Thanks [@tony-sull](https://github.com/tony-sull)! - Adding the `withastro` keyword to include the adapters on the [Integrations Catalog](https://astro.build/integrations) + +## 1.0.0 + +### Major Changes + +- [`04ad44563`](https://github.com/withastro/astro/commit/04ad445632c67bdd60c1704e1e0dcbcaa27b9308) - > Astro v1.0 is out! Read the [official announcement post](https://astro.build/blog/astro-1/). + + **No breaking changes**. This package is now officially stable and compatible with `astro@1.0.0`! + +### Patch Changes + +- Updated dependencies [[`04ad44563`](https://github.com/withastro/astro/commit/04ad445632c67bdd60c1704e1e0dcbcaa27b9308)]: + - @astrojs/webapi@1.0.0 + +## 0.2.1 + +### Patch Changes + +- [#4055](https://github.com/withastro/astro/pull/4055) [`44694d8a9`](https://github.com/withastro/astro/commit/44694d8a9084bb1b09840ec8967edd75fa033174) Thanks [@matthewp](https://github.com/matthewp)! - Handle binary data request bodies in the Node adapter + +## 0.2.0 + +### Minor Changes + +- [#4015](https://github.com/withastro/astro/pull/4015) [`6fd161d76`](https://github.com/withastro/astro/commit/6fd161d7691cbf9d3ffa4646e46059dfd0940010) Thanks [@matthewp](https://github.com/matthewp)! - New `output` configuration option + + This change introduces a new "output target" configuration option (`output`). Setting the output target lets you decide the format of your final build, either: + + - `"static"` (default): A static site. Your final build will be a collection of static assets (HTML, CSS, JS) that you can deploy to any static site host. + - `"server"`: A dynamic server application. Your final build will be an application that will run in a hosted server environment, generating HTML dynamically for different requests. + + If `output` is omitted from your config, the default value `"static"` will be used. + + 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: + + ```diff + import { defineConfig } from 'astro/config'; + import netlify from '@astrojs/netlify/functions'; + + export default defineConfig({ + adapter: netlify(), + + output: 'server', + }); + ``` + +* [#3973](https://github.com/withastro/astro/pull/3973) [`5a23483ef`](https://github.com/withastro/astro/commit/5a23483efb3ba614b05a00064f84415620605204) Thanks [@matthewp](https://github.com/matthewp)! - Adds support for Astro.clientAddress + + The new `Astro.clientAddress` property allows you to get the IP address of the requested user. + + ```astro + + ``` + + This property is only available when building for SSR, and only if the adapter you are using supports providing the IP address. If you attempt to access the property in a SSG app it will throw an error. + +### Patch Changes + +- [#4023](https://github.com/withastro/astro/pull/4023) [`4ca6a0933`](https://github.com/withastro/astro/commit/4ca6a0933d92dd559327dd46a28712d918caebf7) Thanks [@matthewp](https://github.com/matthewp)! - Fixes Node adapter to accept a request body + +## 0.1.6 + +### Patch Changes + +- [#3885](https://github.com/withastro/astro/pull/3885) [`bf5d1cc1e`](https://github.com/withastro/astro/commit/bf5d1cc1e71da38a14658c615e9481f2145cc6e7) Thanks [@delucis](https://github.com/delucis)! - Integration README fixes + +## 0.1.5 + +### Patch Changes + +- [#3865](https://github.com/withastro/astro/pull/3865) [`1f9e4857`](https://github.com/withastro/astro/commit/1f9e4857ff2b2cb7db89d619618cdf546cd3b3dc) Thanks [@delucis](https://github.com/delucis)! - Small README fixes + +* [#3854](https://github.com/withastro/astro/pull/3854) [`b012ee55`](https://github.com/withastro/astro/commit/b012ee55b107dea0730286263b27d83e530fad5d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - [astro add] Support adapters and third party packages + +## 0.1.4 + +### Patch Changes + +- [#3817](https://github.com/withastro/astro/pull/3817) [`2f56664f`](https://github.com/withastro/astro/commit/2f56664f85596c6268ecb44bbb9c36cca2ea49c5) Thanks [@ran-dall](https://github.com/ran-dall)! - Fix example on `README.md` + +## 0.1.3 + +### Patch Changes + +- [#3677](https://github.com/withastro/astro/pull/3677) [`8045c8ad`](https://github.com/withastro/astro/commit/8045c8ade16fe4306448b7f98a4560ef0557d378) Thanks [@Jutanium](https://github.com/Jutanium)! - Update READMEs + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`4de53ecc`](https://github.com/withastro/astro/commit/4de53eccef346bed843b491b7ab93987d7d85655)]: + - @astrojs/webapi@0.12.0 + +## 0.1.1 + +### Patch Changes + +- [`815d62f1`](https://github.com/withastro/astro/commit/815d62f151a36fef7d09590d4962ca71bda61b32) Thanks [@FredKSchott](https://github.com/FredKSchott)! - no changes. + +## 0.1.0 + +### Minor Changes + +- [#2979](https://github.com/withastro/astro/pull/2979) [`9d7a4b59`](https://github.com/withastro/astro/commit/9d7a4b59b53f8cb274266f5036d1cef841750252) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Welcome to the Astro v1.0.0 Beta! Read the [official announcement](https://astro.build/blog/astro-1-beta-release/) for more details. + +## 0.0.2 + +### Patch Changes + +- [#2879](https://github.com/withastro/astro/pull/2879) [`80034c6c`](https://github.com/withastro/astro/commit/80034c6cbc89761618847e6df43fd49560a05aa9) Thanks [@matthewp](https://github.com/matthewp)! - Netlify Adapter + + This change adds a Netlify adapter that uses Netlify Functions. You can use it like so: + + ```js + import { defineConfig } from 'astro/config'; + import netlify from '@astrojs/netlify/functions'; + + export default defineConfig({ + adapter: netlify(), + }); + ``` + +* [#2873](https://github.com/withastro/astro/pull/2873) [`e4025d1f`](https://github.com/withastro/astro/commit/e4025d1f530310d6ab951109f4f53878a307471a) Thanks [@matthewp](https://github.com/matthewp)! - Improves the build by building to a single file for rendering + +## 0.0.2-next.0 + +### Patch Changes + +- [#2873](https://github.com/withastro/astro/pull/2873) [`e4025d1f`](https://github.com/withastro/astro/commit/e4025d1f530310d6ab951109f4f53878a307471a) Thanks [@matthewp](https://github.com/matthewp)! - Improves the build by building to a single file for rendering diff --git a/packages/integrations/node/README.md b/packages/integrations/node/README.md index e02111adc..23ce77c2e 100644 --- a/packages/integrations/node/README.md +++ b/packages/integrations/node/README.md @@ -1,3 +1,38 @@ # @astrojs/node -The Node adapter package has moved. Please see [the new repository for the Node adapter](https://github.com/withastro/adapters/tree/main/packages/node). +This adapter allows Astro to deploy your SSR site to Node targets. + +## Documentation + +Read the [`@astrojs/node` docs][docs] + +## Support + +- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more! + +- Check our [Astro Integration Documentation][astro-integration] for more on integrations. + +- Submit bug reports and feature requests as [GitHub issues][issues]. + +## Contributing + +This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started: + +- [Contributor Manual][contributing] +- [Code of Conduct][coc] +- [Community Guide][community] + +## License + +MIT + +Copyright (c) 2023–present [Astro][astro] + +[astro]: https://astro.build/ +[docs]: https://docs.astro.build/en/guides/integrations-guide/node/ +[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md +[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md +[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md +[discord]: https://astro.build/chat/ +[issues]: https://github.com/withastro/astro/issues +[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/ diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index df99228ed..45f48a2d9 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -1,8 +1,49 @@ { "name": "@astrojs/node", - "version": "1.0.0", - "private": true, + "description": "Deploy your site to a Node.js server", + "version": "9.0.2", "type": "module", - "keywords": [], - "dont_remove": "This is a placeholder for the sake of the docs smoke test" + "types": "./dist/index.d.ts", + "author": "withastro", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/withastro/adapters.git", + "directory": "packages/node" + }, + "keywords": ["withastro", "astro-adapter"], + "bugs": "https://github.com/withastro/adapters/issues", + "homepage": "https://docs.astro.build/en/guides/integrations-guide/node/", + "exports": { + ".": "./dist/index.js", + "./server.js": "./dist/server.js", + "./preview.js": "./dist/preview.js", + "./package.json": "./package.json" + }, + "files": ["dist"], + "scripts": { + "build": "tsc", + "test": "astro-scripts test \"test/**/*.test.js\"" + }, + "dependencies": { + "send": "^1.1.0", + "server-destroy": "^1.0.1" + }, + "peerDependencies": { + "astro": "^5.0.0" + }, + "devDependencies": { + "@astrojs/test-utils": "workspace:*", + "@types/node": "^22.10.6", + "@types/send": "^0.17.4", + "@types/server-destroy": "^1.0.4", + "astro": "^5.1.6", + "astro-scripts": "workspace:*", + "cheerio": "1.0.0", + "express": "^4.21.2", + "node-mocks-http": "^1.16.2" + }, + "publishConfig": { + "provenance": true + } } diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts new file mode 100644 index 000000000..e91ed171b --- /dev/null +++ b/packages/integrations/node/src/index.ts @@ -0,0 +1,61 @@ +import type { AstroAdapter, AstroIntegration } from 'astro'; +import { AstroError } from 'astro/errors'; +import type { Options, UserOptions } from './types.js'; + +export function getAdapter(options: Options): AstroAdapter { + return { + name: '@astrojs/node', + serverEntrypoint: '@astrojs/node/server.js', + previewEntrypoint: '@astrojs/node/preview.js', + exports: ['handler', 'startServer', 'options'], + args: options, + adapterFeatures: { + buildOutput: 'server', + edgeMiddleware: false, + }, + supportedAstroFeatures: { + hybridOutput: 'stable', + staticOutput: 'stable', + serverOutput: 'stable', + sharpImageService: 'stable', + i18nDomains: 'experimental', + envGetSecret: 'stable', + }, + }; +} + +export default function createIntegration(userOptions: UserOptions): AstroIntegration { + if (!userOptions?.mode) { + throw new AstroError(`Setting the 'mode' option is required.`); + } + + let _options: Options; + return { + name: '@astrojs/node', + hooks: { + 'astro:config:setup': async ({ updateConfig, config }) => { + updateConfig({ + image: { + endpoint: config.image.endpoint ?? 'astro/assets/endpoint/node', + }, + vite: { + ssr: { + noExternal: ['@astrojs/node'], + }, + }, + }); + }, + 'astro:config:done': ({ setAdapter, config }) => { + _options = { + ...userOptions, + client: config.build.client?.toString(), + server: config.build.server?.toString(), + host: config.server.host, + port: config.server.port, + assets: config.build.assets, + }; + setAdapter(getAdapter(_options)); + }, + }, + }; +} diff --git a/packages/integrations/node/src/log-listening-on.ts b/packages/integrations/node/src/log-listening-on.ts new file mode 100644 index 000000000..88c4e9d80 --- /dev/null +++ b/packages/integrations/node/src/log-listening-on.ts @@ -0,0 +1,91 @@ +import type http from 'node:http'; +import https from 'node:https'; +import type { AddressInfo } from 'node:net'; +import os from 'node:os'; +import type { AstroIntegrationLogger } from 'astro'; +import type { Options } from './types.js'; + +const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']); + +export async function logListeningOn( + logger: AstroIntegrationLogger, + server: http.Server | https.Server, + configuredHost: string | boolean | undefined +) { + await new Promise<void>((resolve) => server.once('listening', resolve)); + const protocol = server instanceof https.Server ? 'https' : 'http'; + // Allow to provide host value at runtime + const host = getResolvedHostForHttpServer(configuredHost); + const { port } = server.address() as AddressInfo; + const address = getNetworkAddress(protocol, host, port); + + if (host === undefined || wildcardHosts.has(host)) { + logger.info( + `Server listening on \n local: ${address.local[0]} \t\n network: ${address.network[0]}\n` + ); + } else { + logger.info(`Server listening on ${address.local[0]}`); + } +} + +function getResolvedHostForHttpServer(host: string | boolean | undefined) { + if (host === false) { + // Use a secure default + return 'localhost'; + // biome-ignore lint/style/noUselessElse: <explanation> + } else if (host === true) { + // If passed --host in the CLI without arguments + return undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs) + // biome-ignore lint/style/noUselessElse: <explanation> + } else { + return host; + } +} + +interface NetworkAddressOpt { + local: string[]; + network: string[]; +} + +// this code from vite https://github.com/vitejs/vite/blob/d09bbd093a4b893e78f0bbff5b17c7cf7821f403/packages/vite/src/node/utils.ts#L892-L914 +export function getNetworkAddress( + // biome-ignore lint/style/useDefaultParameterLast: <explanation> + protocol: 'http' | 'https' = 'http', + hostname: string | undefined, + port: number, + base?: string +) { + const NetworkAddress: NetworkAddressOpt = { + local: [], + network: [], + }; + // biome-ignore lint/complexity/noForEach: <explanation> + Object.values(os.networkInterfaces()) + .flatMap((nInterface) => nInterface ?? []) + .filter( + (detail) => + // biome-ignore lint/complexity/useOptionalChain: <explanation> + detail && + detail.address && + (detail.family === 'IPv4' || + // @ts-expect-error Node 18.0 - 18.3 returns number + detail.family === 4) + ) + .forEach((detail) => { + let host = detail.address.replace( + '127.0.0.1', + hostname === undefined || wildcardHosts.has(hostname) ? 'localhost' : hostname + ); + // ipv6 host + if (host.includes(':')) { + host = `[${host}]`; + } + const url = `${protocol}://${host}:${port}${base ? base : ''}`; + if (detail.address.includes('127.0.0.1')) { + NetworkAddress.local.push(url); + } else { + NetworkAddress.network.push(url); + } + }); + return NetworkAddress; +} diff --git a/packages/integrations/node/src/middleware.ts b/packages/integrations/node/src/middleware.ts new file mode 100644 index 000000000..5bb104914 --- /dev/null +++ b/packages/integrations/node/src/middleware.ts @@ -0,0 +1,43 @@ +import type { NodeApp } from 'astro/app/node'; +import { createAppHandler } from './serve-app.js'; +import type { RequestHandler } from './types.js'; + +/** + * Creates a middleware that can be used with Express, Connect, etc. + * + * Similar to `createAppHandler` but can additionally be placed in the express + * chain as an error middleware. + * + * https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling + */ +export default function createMiddleware(app: NodeApp): RequestHandler { + const handler = createAppHandler(app); + const logger = app.getAdapterLogger(); + // using spread args because express trips up if the function's + // stringified body includes req, res, next, locals directly + return async (...args) => { + // assume normal invocation at first + const [req, res, next, locals] = args; + // short circuit if it is an error invocation + if (req instanceof Error) { + const error = req; + if (next) { + return next(error); + // biome-ignore lint/style/noUselessElse: <explanation> + } else { + throw error; + } + } + try { + await handler(req, res, next, locals); + } catch (err) { + logger.error(`Could not render ${req.url}`); + console.error(err); + if (!res.headersSent) { + // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation> + res.writeHead(500, `Server error`); + res.end(); + } + } + }; +} diff --git a/packages/integrations/node/src/polyfill.ts b/packages/integrations/node/src/polyfill.ts new file mode 100644 index 000000000..dc00f45d7 --- /dev/null +++ b/packages/integrations/node/src/polyfill.ts @@ -0,0 +1,3 @@ +import { applyPolyfills } from 'astro/app/node'; + +applyPolyfills(); diff --git a/packages/integrations/node/src/preview.ts b/packages/integrations/node/src/preview.ts new file mode 100644 index 000000000..94a81bfdb --- /dev/null +++ b/packages/integrations/node/src/preview.ts @@ -0,0 +1,69 @@ +import { fileURLToPath } from 'node:url'; +import type { CreatePreviewServer } from 'astro'; +import { AstroError } from 'astro/errors'; +import { logListeningOn } from './log-listening-on.js'; +import type { createExports } from './server.js'; +import { createServer } from './standalone.js'; + +type ServerModule = ReturnType<typeof createExports>; +type MaybeServerModule = Partial<ServerModule>; + +const createPreviewServer: CreatePreviewServer = async (preview) => { + let ssrHandler: ServerModule['handler']; + let options: ServerModule['options']; + try { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + const ssrModule: MaybeServerModule = await import(preview.serverEntrypoint.toString()); + if (typeof ssrModule.handler === 'function') { + ssrHandler = ssrModule.handler; + // biome-ignore lint/style/noNonNullAssertion: <explanation> + options = ssrModule.options!; + } else { + throw new AstroError( + `The server entrypoint doesn't have a handler. Are you sure this is the right file?` + ); + } + } catch (err) { + if ((err as any).code === 'ERR_MODULE_NOT_FOUND') { + throw new AstroError( + `The server entrypoint ${fileURLToPath( + preview.serverEntrypoint + )} does not exist. Have you ran a build yet?` + ); + // biome-ignore lint/style/noUselessElse: <explanation> + } else { + throw err; + } + } + // If the user didn't specify a host, it will already have been defaulted to + // "localhost" by getResolvedHostForHttpServer in astro core/preview/util.ts. + // The value `undefined` actually means that either the user set `options.server.host` + // to `true`, or they passed `--host` without an argument. In that case, we + // should listen on all IPs. + const host = process.env.HOST ?? preview.host ?? '0.0.0.0'; + + const port = preview.port ?? 4321; + const server = createServer(ssrHandler, host, port); + + // If user specified custom headers append a listener + // to the server to add those headers to response + if (preview.headers) { + server.server.addListener('request', (_, res) => { + if (res.statusCode === 200) { + for (const [name, value] of Object.entries(preview.headers ?? {})) { + if (value) res.setHeader(name, value); + } + } + }); + } + + logListeningOn(preview.logger, server.server, host); + await new Promise<void>((resolve, reject) => { + server.server.once('listening', resolve); + server.server.once('error', reject); + server.server.listen(port, host); + }); + return server; +}; + +export { createPreviewServer as default }; diff --git a/packages/integrations/node/src/serve-app.ts b/packages/integrations/node/src/serve-app.ts new file mode 100644 index 000000000..2934a01ab --- /dev/null +++ b/packages/integrations/node/src/serve-app.ts @@ -0,0 +1,52 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import { NodeApp } from 'astro/app/node'; +import type { RequestHandler } from './types.js'; + +/** + * Creates a Node.js http listener for on-demand rendered pages, compatible with http.createServer and Connect middleware. + * If the next callback is provided, it will be called if the request does not have a matching route. + * Intended to be used in both standalone and middleware mode. + */ +export function createAppHandler(app: NodeApp): RequestHandler { + /** + * Keep track of the current request path using AsyncLocalStorage. + * Used to log unhandled rejections with a helpful message. + */ + const als = new AsyncLocalStorage<string>(); + const logger = app.getAdapterLogger(); + process.on('unhandledRejection', (reason) => { + const requestUrl = als.getStore(); + logger.error(`Unhandled rejection while rendering ${requestUrl}`); + console.error(reason); + }); + + return async (req, res, next, locals) => { + let request: Request; + try { + request = NodeApp.createRequest(req); + } catch (err) { + logger.error(`Could not render ${req.url}`); + console.error(err); + res.statusCode = 500; + res.end('Internal Server Error'); + return; + } + + const routeData = app.match(request); + if (routeData) { + const response = await als.run(request.url, () => + app.render(request, { + addCookieHeader: true, + locals, + routeData, + }) + ); + await NodeApp.writeResponse(response, res); + } else if (next) { + return next(); + } else { + const response = await app.render(req); + await NodeApp.writeResponse(response, res); + } + }; +} diff --git a/packages/integrations/node/src/serve-static.ts b/packages/integrations/node/src/serve-static.ts new file mode 100644 index 000000000..c9839ea8b --- /dev/null +++ b/packages/integrations/node/src/serve-static.ts @@ -0,0 +1,135 @@ +import fs from 'node:fs'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import path from 'node:path'; +import url from 'node:url'; +import type { NodeApp } from 'astro/app/node'; +import send from 'send'; +import type { Options } from './types.js'; + +// check for a dot followed by a extension made up of lowercase characters +const isSubresourceRegex = /.+\.[a-z]+$/i; + +/** + * Creates a Node.js http listener for static files and prerendered pages. + * In standalone mode, the static handler is queried first for the static files. + * If one matching the request path is not found, it relegates to the SSR handler. + * Intended to be used only in the standalone mode. + */ +export function createStaticHandler(app: NodeApp, options: Options) { + const client = resolveClientDir(options); + /** + * @param ssr The SSR handler to be called if the static handler does not find a matching file. + */ + return (req: IncomingMessage, res: ServerResponse, ssr: () => unknown) => { + if (req.url) { + const [urlPath, urlQuery] = req.url.split('?'); + const filePath = path.join(client, app.removeBase(urlPath)); + + let isDirectory = false; + try { + isDirectory = fs.lstatSync(filePath).isDirectory(); + } catch {} + + const { trailingSlash = 'ignore' } = options; + + const hasSlash = urlPath.endsWith('/'); + let pathname = urlPath; + + switch (trailingSlash) { + case 'never': { + if (isDirectory && urlPath !== '/' && hasSlash) { + // biome-ignore lint/style/useTemplate: more readable like this + pathname = urlPath.slice(0, -1) + (urlQuery ? '?' + urlQuery : ''); + res.statusCode = 301; + res.setHeader('Location', pathname); + return res.end(); + } + if (isDirectory && !hasSlash) { + pathname = `${urlPath}/index.html`; + } + break; + } + case 'ignore': { + if (isDirectory && !hasSlash) { + pathname = `${urlPath}/index.html`; + } + break; + } + case 'always': { + // trailing slash is not added to "subresources" + if (!hasSlash && !isSubresourceRegex.test(urlPath)) { + // biome-ignore lint/style/useTemplate: more readable like this + pathname = urlPath + '/' + (urlQuery ? '?' + urlQuery : ''); + res.statusCode = 301; + res.setHeader('Location', pathname); + return res.end(); + } + break; + } + } + // app.removeBase sometimes returns a path without a leading slash + pathname = prependForwardSlash(app.removeBase(pathname)); + + const stream = send(req, pathname, { + root: client, + dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny', + }); + + let forwardError = false; + + stream.on('error', (err) => { + if (forwardError) { + console.error(err.toString()); + res.writeHead(500); + res.end('Internal server error'); + return; + } + // File not found, forward to the SSR handler + ssr(); + }); + stream.on('headers', (_res: ServerResponse) => { + // assets in dist/_astro are hashed and should get the immutable header + if (pathname.startsWith(`/${options.assets}/`)) { + // 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 + _res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + } + }); + stream.on('file', () => { + forwardError = true; + }); + stream.pipe(res); + } else { + ssr(); + } + }; +} + +function resolveClientDir(options: Options) { + const clientURLRaw = new URL(options.client); + const serverURLRaw = new URL(options.server); + const rel = path.relative(url.fileURLToPath(serverURLRaw), url.fileURLToPath(clientURLRaw)); + + // walk up the parent folders until you find the one that is the root of the server entry folder. This is how we find the client folder relatively. + const serverFolder = path.basename(options.server); + let serverEntryFolderURL = path.dirname(import.meta.url); + while (!serverEntryFolderURL.endsWith(serverFolder)) { + serverEntryFolderURL = path.dirname(serverEntryFolderURL); + } + // biome-ignore lint/style/useTemplate: <explanation> + const serverEntryURL = serverEntryFolderURL + '/entry.mjs'; + const clientURL = new URL(appendForwardSlash(rel), serverEntryURL); + const client = url.fileURLToPath(clientURL); + return client; +} + +function prependForwardSlash(pth: string) { + // biome-ignore lint/style/useTemplate: <explanation> + return pth.startsWith('/') ? pth : '/' + pth; +} + +function appendForwardSlash(pth: string) { + // biome-ignore lint/style/useTemplate: <explanation> + return pth.endsWith('/') ? pth : pth + '/'; +} diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts new file mode 100644 index 000000000..cef262b47 --- /dev/null +++ b/packages/integrations/node/src/server.ts @@ -0,0 +1,32 @@ +// Keep at the top +import './polyfill.js'; + +import type { SSRManifest } from 'astro'; +import { NodeApp } from 'astro/app/node'; +import { setGetEnv } from 'astro/env/setup'; +import createMiddleware from './middleware.js'; +import { createStandaloneHandler } from './standalone.js'; +import startServer from './standalone.js'; +import type { Options } from './types.js'; + +setGetEnv((key) => process.env[key]); + +export function createExports(manifest: SSRManifest, options: Options) { + const app = new NodeApp(manifest); + options.trailingSlash = manifest.trailingSlash; + return { + options: options, + handler: + options.mode === 'middleware' ? createMiddleware(app) : createStandaloneHandler(app, options), + startServer: () => startServer(app, options), + }; +} + +export function start(manifest: SSRManifest, options: Options) { + if (options.mode !== 'standalone' || process.env.ASTRO_NODE_AUTOSTART === 'disabled') { + return; + } + + const app = new NodeApp(manifest); + startServer(app, options); +} diff --git a/packages/integrations/node/src/standalone.ts b/packages/integrations/node/src/standalone.ts new file mode 100644 index 000000000..fadcc37b4 --- /dev/null +++ b/packages/integrations/node/src/standalone.ts @@ -0,0 +1,93 @@ +import fs from 'node:fs'; +import http from 'node:http'; +import https from 'node:https'; +import type { PreviewServer } from 'astro'; +import type { NodeApp } from 'astro/app/node'; +import enableDestroy from 'server-destroy'; +import { logListeningOn } from './log-listening-on.js'; +import { createAppHandler } from './serve-app.js'; +import { createStaticHandler } from './serve-static.js'; +import type { Options } from './types.js'; + +// Used to get Host Value at Runtime +export const hostOptions = (host: Options['host']): string => { + if (typeof host === 'boolean') { + return host ? '0.0.0.0' : 'localhost'; + } + return host; +}; + +export default function standalone(app: NodeApp, options: Options) { + const port = process.env.PORT ? Number(process.env.PORT) : (options.port ?? 8080); + const host = process.env.HOST ?? hostOptions(options.host); + const handler = createStandaloneHandler(app, options); + const server = createServer(handler, host, port); + server.server.listen(port, host); + if (process.env.ASTRO_NODE_LOGGING !== 'disabled') { + logListeningOn(app.getAdapterLogger(), server.server, host); + } + return { + server, + done: server.closed(), + }; +} + +// also used by server entrypoint +export function createStandaloneHandler(app: NodeApp, options: Options) { + const appHandler = createAppHandler(app); + const staticHandler = createStaticHandler(app, options); + return (req: http.IncomingMessage, res: http.ServerResponse) => { + try { + // validate request path + // biome-ignore lint/style/noNonNullAssertion: <explanation> + decodeURI(req.url!); + } catch { + res.writeHead(400); + res.end('Bad request.'); + return; + } + staticHandler(req, res, () => appHandler(req, res)); + }; +} + +// also used by preview entrypoint +export function createServer(listener: http.RequestListener, host: string, port: number) { + let httpServer: http.Server | https.Server; + + if (process.env.SERVER_CERT_PATH && process.env.SERVER_KEY_PATH) { + httpServer = https.createServer( + { + key: fs.readFileSync(process.env.SERVER_KEY_PATH), + cert: fs.readFileSync(process.env.SERVER_CERT_PATH), + }, + listener + ); + } else { + httpServer = http.createServer(listener); + } + enableDestroy(httpServer); + + // Resolves once the server is closed + const closed = new Promise<void>((resolve, reject) => { + httpServer.addListener('close', resolve); + httpServer.addListener('error', reject); + }); + + const previewable = { + host, + port, + closed() { + return closed; + }, + async stop() { + await new Promise((resolve, reject) => { + httpServer.destroy((err) => (err ? reject(err) : resolve(undefined))); + }); + }, + } satisfies PreviewServer; + + return { + server: httpServer, + ...previewable, + }; +} diff --git a/packages/integrations/node/src/types.ts b/packages/integrations/node/src/types.ts new file mode 100644 index 000000000..010053de5 --- /dev/null +++ b/packages/integrations/node/src/types.ts @@ -0,0 +1,39 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import type { SSRManifest } from 'astro'; +import type { NodeApp } from 'astro/app/node'; + +export interface UserOptions { + /** + * Specifies the mode that the adapter builds to. + * + * - 'middleware' - Build to middleware, to be used within another Node.js server, such as Express. + * - 'standalone' - Build to a standalone server. The server starts up just by running the built script. + */ + mode: 'middleware' | 'standalone'; +} + +export interface Options extends UserOptions { + host: string | boolean; + port: number; + server: string; + client: string; + assets: string; + trailingSlash?: SSRManifest['trailingSlash']; +} + +export interface CreateServerOptions { + app: NodeApp; + assets: string; + client: URL; + port: number; + host: string | undefined; + removeBase: (pathname: string) => string; +} + +export type RequestHandler = (...args: RequestHandlerParams) => void | Promise<void>; +export type RequestHandlerParams = [ + req: IncomingMessage, + res: ServerResponse, + next?: (err?: unknown) => void, + locals?: object, +]; diff --git a/packages/integrations/node/test/api-route.test.js b/packages/integrations/node/test/api-route.test.js new file mode 100644 index 000000000..05cdcd637 --- /dev/null +++ b/packages/integrations/node/test/api-route.test.js @@ -0,0 +1,153 @@ +import * as assert from 'node:assert/strict'; +import crypto from 'node:crypto'; +import { after, before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('API routes', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + /** @type {import('../../../astro/src/types/public/preview.js').PreviewServer} */ + let previewServer; + /** @type {URL} */ + let baseUri; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/api-route/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + previewServer = await fixture.preview(); + baseUri = new URL(`http://${previewServer.host ?? 'localhost'}:${previewServer.port}/`); + }); + + after(() => previewServer.stop()); + + it('Can get the request body', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + method: 'POST', + url: '/recipes', + }); + + req.once('async_iterator', () => { + req.send(JSON.stringify({ id: 2 })); + }); + + handler(req, res); + + const [buffer] = await done; + + const json = JSON.parse(buffer.toString('utf-8')); + + assert.equal(json.length, 1); + + assert.equal(json[0].name, 'Broccoli Soup'); + }); + + it('Can get binary data', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + + const { req, res, done } = createRequestAndResponse({ + method: 'POST', + url: '/binary', + }); + + req.once('async_iterator', () => { + req.send(Buffer.from(new Uint8Array([1, 2, 3, 4, 5]))); + }); + + handler(req, res); + + const [out] = await done; + const arr = Array.from(new Uint8Array(out.buffer)); + assert.deepEqual(arr, [5, 4, 3, 2, 1]); + }); + + it('Can post large binary data', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + + const { req, res, done } = createRequestAndResponse({ + method: 'POST', + url: '/hash', + }); + + handler(req, res); + + let expectedDigest = null; + req.once('async_iterator', () => { + // Send 256MB of garbage data in 256KB chunks. This should be fast (< 1sec). + let remainingBytes = 256 * 1024 * 1024; + const chunkSize = 256 * 1024; + + const hash = crypto.createHash('sha256'); + while (remainingBytes > 0) { + const size = Math.min(remainingBytes, chunkSize); + const chunk = Buffer.alloc(size, Math.floor(Math.random() * 256)); + hash.update(chunk); + req.emit('data', chunk); + remainingBytes -= size; + } + + req.emit('end'); + expectedDigest = hash.digest(); + }); + + const [out] = await done; + assert.deepEqual(new Uint8Array(out.buffer), new Uint8Array(expectedDigest)); + }); + + it('Can bail on streaming', async () => { + const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + url: '/streaming', + }); + + const locals = { cancelledByTheServer: false }; + + handler(req, res, () => {}, locals); + req.send(); + + await new Promise((resolve) => setTimeout(resolve, 500)); + res.emit('close'); + + await done; + + assert.deepEqual(locals, { cancelledByTheServer: true }); + }); + + it('Can respond with SSR redirect', async () => { + const controller = new AbortController(); + setTimeout(() => controller.abort(), 1000); + const response = await fetch(new URL('/redirect', baseUri), { + redirect: 'manual', + signal: controller.signal, + }); + assert.equal(response.status, 302); + assert.equal(response.headers.get('location'), '/destination'); + }); + + it('Can respond with Astro.redirect', async () => { + const controller = new AbortController(); + setTimeout(() => controller.abort(), 1000); + const response = await fetch(new URL('/astro-redirect', baseUri), { + redirect: 'manual', + signal: controller.signal, + }); + assert.equal(response.status, 303); + assert.equal(response.headers.get('location'), '/destination'); + }); + + it('Can respond with Response.redirect', async () => { + const controller = new AbortController(); + setTimeout(() => controller.abort(), 1000); + const response = await fetch(new URL('/response-redirect', baseUri), { + redirect: 'manual', + signal: controller.signal, + }); + assert.equal(response.status, 307); + assert.equal(response.headers.get('location'), String(new URL('/destination', baseUri))); + }); +}); diff --git a/packages/integrations/node/test/assets.test.js b/packages/integrations/node/test/assets.test.js new file mode 100644 index 000000000..0b71f94cd --- /dev/null +++ b/packages/integrations/node/test/assets.test.js @@ -0,0 +1,44 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +describe('Assets', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devPreview; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/image/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + vite: { + build: { + assetsInlineLimit: 0, + }, + }, + }); + await fixture.build(); + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + it('Assets within the _astro folder should be given immutable headers', async () => { + let response = await fixture.fetch('/text-file'); + let cacheControl = response.headers.get('cache-control'); + assert.equal(cacheControl, null); + const html = await response.text(); + const $ = cheerio.load(html); + + // Fetch the asset + const fileURL = $('a').attr('href'); + response = await fixture.fetch(fileURL); + cacheControl = response.headers.get('cache-control'); + assert.equal(cacheControl, 'public, max-age=31536000, immutable'); + }); +}); diff --git a/packages/integrations/node/test/bad-urls.test.js b/packages/integrations/node/test/bad-urls.test.js new file mode 100644 index 000000000..cdc0158ff --- /dev/null +++ b/packages/integrations/node/test/bad-urls.test.js @@ -0,0 +1,49 @@ +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'; + +describe('Bad URLs', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devPreview; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/bad-urls/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + it('Does not crash on bad urls', async () => { + const weirdURLs = [ + '/\\xfs.bxss.me%3Fastrojs.com/hello-world', + '/asdasdasd@ax_zX=.zxczas🐥%/úadasd000%/', + '%', + '%80', + '%c', + '%c0%80', + '%20foobar%', + ]; + + const statusCodes = [400, 404, 500]; + for (const weirdUrl of weirdURLs) { + const fetchResult = await fixture.fetch(weirdUrl); + assert.equal( + statusCodes.includes(fetchResult.status), + true, + `${weirdUrl} returned something else than 400, 404, or 500` + ); + } + const stillWork = await fixture.fetch('/'); + const text = await stillWork.text(); + assert.equal(text, '<!DOCTYPE html>Hello!'); + }); +}); diff --git a/packages/integrations/node/test/encoded.test.js b/packages/integrations/node/test/encoded.test.js new file mode 100644 index 000000000..4fc97cf7f --- /dev/null +++ b/packages/integrations/node/test/encoded.test.js @@ -0,0 +1,45 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('Encoded Pathname', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/encoded/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + }); + + it('Can get an Astro file', async () => { + const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + url: '/什么', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('什么</h1>'), true); + }); + + it('Can get a Markdown file', async () => { + const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs'); + + const { req, res, text } = createRequestAndResponse({ + url: '/blog/什么', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('什么</h1>'), true); + }); +}); diff --git a/packages/integrations/node/test/errors.test.js b/packages/integrations/node/test/errors.test.js new file mode 100644 index 000000000..9bf4aa29b --- /dev/null +++ b/packages/integrations/node/test/errors.test.js @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { Worker } from 'node:worker_threads'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +describe('Errors', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/errors/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + }); + let devPreview; + + // biome-ignore lint/suspicious/noDuplicateTestHooks: <explanation> + before(async () => { + // The two tests that need the server to run are skipped + // devPreview = await fixture.preview(); + }); + after(async () => { + await devPreview?.stop(); + }); + + it('stays alive after offshoot promise rejections', async () => { + // this test needs to happen in a worker because node test runner adds a listener for unhandled rejections in the main thread + const url = new URL('./fixtures/errors/dist/server/entry.mjs', import.meta.url); + const worker = new Worker(fileURLToPath(url), { + type: 'module', + env: { ASTRO_NODE_LOGGING: 'enabled' }, + }); + + await new Promise((resolve, reject) => { + worker.stdout.on('data', (data) => { + setTimeout(() => reject('Server took too long to start'), 1000); + if (data.toString().includes('Server listening on http://localhost:4321')) resolve(); + }); + }); + + await fetch('http://localhost:4321/offshoot-promise-rejection'); + + // if there was a crash, it becomes an error here + await worker.terminate(); + }); + + it( + 'rejected promise in template', + { skip: true, todo: 'Review the response from the in-stream' }, + async () => { + const res = await fixture.fetch('/in-stream'); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal($('p').text().trim(), 'Internal server error'); + } + ); + + it( + 'generator that throws called in template', + { skip: true, todo: 'Review the response from the generator' }, + async () => { + const result = ['<!DOCTYPE html><h1>Astro</h1> 1', 'Internal server error']; + + /** @type {Response} */ + const res = await fixture.fetch('/generator'); + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + const chunk1 = await reader.read(); + const chunk2 = await reader.read(); + const chunk3 = await reader.read(); + assert.equal(chunk1.done, false); + console.log(chunk1); + console.log(chunk2); + console.log(chunk3); + if (chunk2.done) { + assert.equal(decoder.decode(chunk1.value), result.join('')); + } else if (chunk3.done) { + assert.equal(decoder.decode(chunk1.value), result[0]); + assert.equal(decoder.decode(chunk2.value), result[1]); + } else { + throw new Error('The response should take at most 2 chunks.'); + } + } + ); +}); diff --git a/packages/integrations/node/test/fixtures/api-route/astro.config.mjs b/packages/integrations/node/test/fixtures/api-route/astro.config.mjs new file mode 100644 index 000000000..3eada8758 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig} from "astro/config"; + +export default defineConfig({ + security: { + checkOrigin: false + } +})
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/api-route/package.json b/packages/integrations/node/test/fixtures/api-route/package.json new file mode 100644 index 000000000..d2a97c8c7 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-api-route", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/astro-redirect.astro b/packages/integrations/node/test/fixtures/api-route/src/pages/astro-redirect.astro new file mode 100644 index 000000000..65a8765e8 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/astro-redirect.astro @@ -0,0 +1,3 @@ +--- +return Astro.redirect('/destination', 303); +--- diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/binary.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/binary.ts new file mode 100644 index 000000000..b1c7ce263 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/binary.ts @@ -0,0 +1,11 @@ + +export async function POST({ request }: { request: Request }) { + let body = await request.arrayBuffer(); + let data = new Uint8Array(body); + let r = data.reverse(); + return new Response(r, { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts new file mode 100644 index 000000000..3f1b236de --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/hash.ts @@ -0,0 +1,16 @@ +import crypto from 'node:crypto'; + +export async function POST({ request }: { request: Request }) { + const hash = crypto.createHash('sha256'); + + const iterable = request.body as unknown as AsyncIterable<Uint8Array>; + for await (const chunk of iterable) { + hash.update(chunk); + } + + return new Response(hash.digest(), { + headers: { + 'Content-Type': 'application/octet-stream' + } + }); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js b/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js new file mode 100644 index 000000000..7297b9643 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/recipes.js @@ -0,0 +1,24 @@ + +export async function POST({ request }) { + let body = await request.json(); + const recipes = [ + { + id: 1, + name: 'Potato Soup' + }, + { + id: 2, + name: 'Broccoli Soup' + } + ]; + + let out = recipes.filter(r => { + return r.id === body.id; + }); + + return new Response(JSON.stringify(out), { + headers: { + 'Content-Type': 'application/json' + } + }); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/redirect.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/redirect.ts new file mode 100644 index 000000000..36f0e8cf4 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/redirect.ts @@ -0,0 +1,5 @@ +import type { APIContext } from 'astro'; + +export async function GET({ redirect }: APIContext) { + return redirect('/destination'); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/response-redirect.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/response-redirect.ts new file mode 100644 index 000000000..cf16fb941 --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/response-redirect.ts @@ -0,0 +1,5 @@ +import type { APIContext } from 'astro'; + +export async function GET({ url: requestUrl }: APIContext) { + return Response.redirect(new URL('/destination', requestUrl), 307); +} diff --git a/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts b/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts new file mode 100644 index 000000000..9ecb884bf --- /dev/null +++ b/packages/integrations/node/test/fixtures/api-route/src/pages/streaming.ts @@ -0,0 +1,22 @@ +export const GET = ({ locals }) => { + let sentChunks = 0; + + const readableStream = new ReadableStream({ + async pull(controller) { + if (sentChunks === 3) return controller.close(); + else sentChunks++; + + await new Promise(resolve => setTimeout(resolve, 1000)); + controller.enqueue(new TextEncoder().encode('hello\n')); + }, + cancel() { + locals.cancelledByTheServer = true; + } + }); + + return new Response(readableStream, { + headers: { + "Content-Type": "text/event-stream" + } + }); +} diff --git a/packages/integrations/node/test/fixtures/bad-urls/package.json b/packages/integrations/node/test/fixtures/bad-urls/package.json new file mode 100644 index 000000000..14525a047 --- /dev/null +++ b/packages/integrations/node/test/fixtures/bad-urls/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-badurls", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/bad-urls/src/pages/index.astro b/packages/integrations/node/test/fixtures/bad-urls/src/pages/index.astro new file mode 100644 index 000000000..10ddd6d25 --- /dev/null +++ b/packages/integrations/node/test/fixtures/bad-urls/src/pages/index.astro @@ -0,0 +1 @@ +Hello! diff --git a/packages/integrations/node/test/fixtures/encoded/package.json b/packages/integrations/node/test/fixtures/encoded/package.json new file mode 100644 index 000000000..53dc0855f --- /dev/null +++ b/packages/integrations/node/test/fixtures/encoded/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-encoded", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/encoded/src/pages/blog/什么.md b/packages/integrations/node/test/fixtures/encoded/src/pages/blog/什么.md new file mode 100644 index 000000000..2820cf17e --- /dev/null +++ b/packages/integrations/node/test/fixtures/encoded/src/pages/blog/什么.md @@ -0,0 +1 @@ +# 什么 diff --git a/packages/integrations/node/test/fixtures/encoded/src/pages/什么.astro b/packages/integrations/node/test/fixtures/encoded/src/pages/什么.astro new file mode 100644 index 000000000..c8473f594 --- /dev/null +++ b/packages/integrations/node/test/fixtures/encoded/src/pages/什么.astro @@ -0,0 +1 @@ +<h1>什么</h1> diff --git a/packages/integrations/node/test/fixtures/errors/package.json b/packages/integrations/node/test/fixtures/errors/package.json new file mode 100644 index 000000000..9ba94b5af --- /dev/null +++ b/packages/integrations/node/test/fixtures/errors/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-errors", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/errors/src/pages/generator.astro b/packages/integrations/node/test/fixtures/errors/src/pages/generator.astro new file mode 100644 index 000000000..65b8ae62c --- /dev/null +++ b/packages/integrations/node/test/fixtures/errors/src/pages/generator.astro @@ -0,0 +1,11 @@ +--- +function * generator () { + yield 1 + throw Error('ohnoes') +} +--- +<h1>Astro</h1> +{generator()} +<footer> + Footer +</footer>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/errors/src/pages/in-stream.astro b/packages/integrations/node/test/fixtures/errors/src/pages/in-stream.astro new file mode 100644 index 000000000..b7ee6b4ef --- /dev/null +++ b/packages/integrations/node/test/fixtures/errors/src/pages/in-stream.astro @@ -0,0 +1,13 @@ +--- +--- +<html> + <head> + <title>One</title> + </head> + <body> + <h1>One</h1> + <p> + {Promise.reject('Error in the stream')} + </p> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/errors/src/pages/offshoot-promise-rejection.astro b/packages/integrations/node/test/fixtures/errors/src/pages/offshoot-promise-rejection.astro new file mode 100644 index 000000000..be702d5ef --- /dev/null +++ b/packages/integrations/node/test/fixtures/errors/src/pages/offshoot-promise-rejection.astro @@ -0,0 +1,2 @@ +{new Promise(async _ => (await {}, Astro.props.undefined.alsoAPropertyOfUndefined))} +{Astro.props.undefined.propertyOfUndefined}
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/package.json b/packages/integrations/node/test/fixtures/headers/package.json new file mode 100644 index 000000000..8746f772b --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-headers", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-multi.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-multi.astro new file mode 100644 index 000000000..a9ff193df --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-multi.astro @@ -0,0 +1,5 @@ +--- +Astro.cookies.set('from1', 'astro1'); +Astro.cookies.set('from2', 'astro2'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-single.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-single.astro new file mode 100644 index 000000000..c469fd66f --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-cookies-single.astro @@ -0,0 +1,4 @@ +--- +Astro.cookies.set('from1', 'astro1'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-multi.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-multi.astro new file mode 100644 index 000000000..91244e838 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-multi.astro @@ -0,0 +1,7 @@ +--- +Astro.response.headers.append('set-cookie', 'from1=response1'); +Astro.response.headers.append('set-cookie', 'from2=response2'); +Astro.cookies.set('from3', 'astro1'); +Astro.cookies.set('from4', 'astro2'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-single.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-single.astro new file mode 100644 index 000000000..97719dfa9 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-astro-response-cookie-single.astro @@ -0,0 +1,5 @@ +--- +Astro.response.headers.append('set-cookie', 'from1=response1'); +Astro.cookies.set('from1', 'astro1'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-multi.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-multi.astro new file mode 100644 index 000000000..133cbd423 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-multi.astro @@ -0,0 +1,5 @@ +--- +Astro.response.headers.append('set-cookie', 'from1=value1'); +Astro.response.headers.append('set-cookie', 'from2=value2'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-single.astro b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-single.astro new file mode 100644 index 000000000..dc76082db --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/astro/component-response-cookies-single.astro @@ -0,0 +1,4 @@ +--- +Astro.response.headers.append('set-cookie', 'from1=value1'); +--- +<p>hello world</p>
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-multi.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-multi.ts new file mode 100644 index 000000000..aaae88e59 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-multi.ts @@ -0,0 +1,9 @@ +import type { APIContext } from 'astro'; + +export async function GET({ request, cookies }: APIContext) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + cookies.set('from1', 'astro1'); + cookies.set('from2', 'astro2'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-single.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-single.ts new file mode 100644 index 000000000..03e74c604 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-cookies-single.ts @@ -0,0 +1,8 @@ +import type { APIContext } from 'astro'; + +export async function GET({ request, cookies }: APIContext) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + cookies.set('from1', 'astro1'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-multi.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-multi.ts new file mode 100644 index 000000000..36906da3a --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-multi.ts @@ -0,0 +1,11 @@ +import type { APIContext } from 'astro'; + +export async function GET({ request, cookies }: APIContext) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('set-cookie', 'from1=response1'); + headers.append('set-cookie', 'from2=response2'); + cookies.set('from3', 'astro1'); + cookies.set('from4', 'astro2'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-single.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-single.ts new file mode 100644 index 000000000..3c1fc4775 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/astro-response-cookie-single.ts @@ -0,0 +1,9 @@ +import type { APIContext } from 'astro'; + +export async function GET({ request, cookies }: APIContext) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('set-cookie', 'from1=response1'); + cookies.set('from1', 'astro1'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/kitchen-sink.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/kitchen-sink.ts new file mode 100644 index 000000000..fb7c30cbc --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/kitchen-sink.ts @@ -0,0 +1,11 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('x-SINGLE', 'single'); + headers.append('X-triple', 'one'); + headers.append('x-Triple', 'two'); + headers.append('x-TRIPLE', 'three'); + headers.append('SET-cookie', 'hello1=world1'); + headers.append('Set-Cookie', 'hello2=world2'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-multi.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-multi.ts new file mode 100644 index 000000000..d974737ee --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-multi.ts @@ -0,0 +1,7 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('Set-Cookie', 'hello1=world1'); + headers.append('SET-COOKIE', 'hello2=world2'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-single.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-single.ts new file mode 100644 index 000000000..f543ae062 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-cookies-single.ts @@ -0,0 +1,6 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('Set-Cookie', 'hello1=world1'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-empty-headers-object.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-empty-headers-object.ts new file mode 100644 index 000000000..b8a9e122e --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-empty-headers-object.ts @@ -0,0 +1,4 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-undefined-headers-object.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-undefined-headers-object.ts new file mode 100644 index 000000000..72f7af071 --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/response-undefined-headers-object.ts @@ -0,0 +1,3 @@ +export async function GET({ request }: { request: Request }) { + return new Response('hello world', { headers: undefined }); +} diff --git a/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/simple.ts b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/simple.ts new file mode 100644 index 000000000..9c6bcacaa --- /dev/null +++ b/packages/integrations/node/test/fixtures/headers/src/pages/endpoints/simple.ts @@ -0,0 +1,6 @@ +export async function GET({ request }: { request: Request }) { + const headers = new Headers(); + headers.append('content-type', 'text/plain;charset=utf-8'); + headers.append('X-HELLO', 'world'); + return new Response('hello world', { headers }); +} diff --git a/packages/integrations/node/test/fixtures/image/package.json b/packages/integrations/node/test/fixtures/image/package.json new file mode 100644 index 000000000..8e4407688 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/package.json @@ -0,0 +1,16 @@ +{ + "name": "@test/nodejs-image", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + }, + "peerDependencies": { + "sharp": "^0.33.5" + }, + "scripts": { + "build": "astro build", + "preview": "astro preview" + } +}
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/image/src/assets/file.txt b/packages/integrations/node/test/fixtures/image/src/assets/file.txt new file mode 100644 index 000000000..e9ea42a12 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/src/assets/file.txt @@ -0,0 +1 @@ +this is a text file diff --git a/packages/integrations/node/test/fixtures/image/src/assets/some_penguin.png b/packages/integrations/node/test/fixtures/image/src/assets/some_penguin.png Binary files differnew file mode 100644 index 000000000..a09d7f894 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/src/assets/some_penguin.png diff --git a/packages/integrations/node/test/fixtures/image/src/pages/[...catchall].astro b/packages/integrations/node/test/fixtures/image/src/pages/[...catchall].astro new file mode 100644 index 000000000..44edaee73 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/src/pages/[...catchall].astro @@ -0,0 +1,8 @@ +--- +import { Image } from "astro:assets"; +import penguin from "../assets/some_penguin.png"; +export const prerender = false; +--- + +<Image src={penguin} alt="Penguins" width={50} /> +<Image src="https://images.unsplash.com/photo-1707229190979-f2d99f7823a8?q=80&w=800&auto=format&fit=crop" alt="Cornwall" width={400} height={300} /> diff --git a/packages/integrations/node/test/fixtures/image/src/pages/text-file.astro b/packages/integrations/node/test/fixtures/image/src/pages/text-file.astro new file mode 100644 index 000000000..893250360 --- /dev/null +++ b/packages/integrations/node/test/fixtures/image/src/pages/text-file.astro @@ -0,0 +1,14 @@ +--- +import txt from '../assets/file.txt?url'; +--- +<html> + <head> + <title>Testing</title> + </head> + <body> + <h1>Testing</h1> + <main> + <a href={txt} download>Download text file</a> + </main> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/locals/astro.config.mjs b/packages/integrations/node/test/fixtures/locals/astro.config.mjs new file mode 100644 index 000000000..3cdf86221 --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/astro.config.mjs @@ -0,0 +1,7 @@ +import {defineConfig} from "astro/config"; + +export default defineConfig({ + security: { + checkOrigin:false + } +})
\ No newline at end of file diff --git a/packages/integrations/node/test/fixtures/locals/package.json b/packages/integrations/node/test/fixtures/locals/package.json new file mode 100644 index 000000000..db854caac --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/locals", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/locals/src/middleware.ts b/packages/integrations/node/test/fixtures/locals/src/middleware.ts new file mode 100644 index 000000000..e349ca41d --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/src/middleware.ts @@ -0,0 +1,6 @@ +import { defineMiddleware } from 'astro:middleware'; + +export const onRequest = defineMiddleware(({ url, locals }, next) => { + if (url.pathname === "/from-astro-middleware") locals.foo = "baz"; + return next(); +}) diff --git a/packages/integrations/node/test/fixtures/locals/src/pages/api.js b/packages/integrations/node/test/fixtures/locals/src/pages/api.js new file mode 100644 index 000000000..3c279e37b --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/src/pages/api.js @@ -0,0 +1,10 @@ + +export async function POST({ locals }) { + const out = { ...locals }; + + return new Response(JSON.stringify(out), { + headers: { + 'Content-Type': 'application/json' + } + }); +} diff --git a/packages/integrations/node/test/fixtures/locals/src/pages/from-astro-middleware.astro b/packages/integrations/node/test/fixtures/locals/src/pages/from-astro-middleware.astro new file mode 100644 index 000000000..224a875ec --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/src/pages/from-astro-middleware.astro @@ -0,0 +1,4 @@ +--- +const { foo } = Astro.locals; +--- +<h1>{foo}</h1> diff --git a/packages/integrations/node/test/fixtures/locals/src/pages/from-node-middleware.astro b/packages/integrations/node/test/fixtures/locals/src/pages/from-node-middleware.astro new file mode 100644 index 000000000..224a875ec --- /dev/null +++ b/packages/integrations/node/test/fixtures/locals/src/pages/from-node-middleware.astro @@ -0,0 +1,4 @@ +--- +const { foo } = Astro.locals; +--- +<h1>{foo}</h1> diff --git a/packages/integrations/node/test/fixtures/node-middleware/package.json b/packages/integrations/node/test/fixtures/node-middleware/package.json new file mode 100644 index 000000000..336e69fef --- /dev/null +++ b/packages/integrations/node/test/fixtures/node-middleware/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/node-middleware", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/node-middleware/src/pages/404.astro b/packages/integrations/node/test/fixtures/node-middleware/src/pages/404.astro new file mode 100644 index 000000000..79f4944bc --- /dev/null +++ b/packages/integrations/node/test/fixtures/node-middleware/src/pages/404.astro @@ -0,0 +1,13 @@ +--- +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>404</title> +</head> +<body>Page does not exist</body> +</html> diff --git a/packages/integrations/node/test/fixtures/node-middleware/src/pages/index.astro b/packages/integrations/node/test/fixtures/node-middleware/src/pages/index.astro new file mode 100644 index 000000000..28ff7d223 --- /dev/null +++ b/packages/integrations/node/test/fixtures/node-middleware/src/pages/index.astro @@ -0,0 +1,11 @@ +--- +--- + +<html lang="en"> +<head><title>node-middleware</title></head> +<style> +</style> +<body> +<div>1</div> +</body> +</html> diff --git a/packages/integrations/node/test/fixtures/node-middleware/src/pages/ssr.ts b/packages/integrations/node/test/fixtures/node-middleware/src/pages/ssr.ts new file mode 100644 index 000000000..423db341a --- /dev/null +++ b/packages/integrations/node/test/fixtures/node-middleware/src/pages/ssr.ts @@ -0,0 +1,7 @@ +export async function GET() { + let number = Math.random(); + return Response.json({ + number, + message: `Here's a random number: ${number}`, + }); +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/package.json b/packages/integrations/node/test/fixtures/prerender-404-500/package.json new file mode 100644 index 000000000..61d70fbb9 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/nodejs-prerender-404-500", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/external-stylesheet.css b/packages/integrations/node/test/fixtures/prerender-404-500/src/external-stylesheet.css new file mode 100644 index 000000000..5f331948a --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/external-stylesheet.css @@ -0,0 +1,3 @@ +body { + background-color: ivory; +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-404.ts b/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-404.ts new file mode 100644 index 000000000..1795c26b0 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-404.ts @@ -0,0 +1,17 @@ +// This module is only used by the prerendered 404.astro. +// It exhibits different behavior if it's called more than once, +// which is detected by a test and interpreted as a failure. + +let usedOnce = false +let dynamicMessage = "Page was not prerendered" + +export default function () { + if (usedOnce === false) { + usedOnce = true + return "Page does not exist" + } + + dynamicMessage += "+" + + return dynamicMessage +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-500.ts b/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-500.ts new file mode 100644 index 000000000..8f8024a60 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/nondeterminism-500.ts @@ -0,0 +1,17 @@ +// This module is only used by the prerendered 500.astro. +// It exhibits different behavior if it's called more than once, +// which is detected by a test and interpreted as a failure. + +let usedOnce = false +let dynamicMessage = "Page was not prerendered" + +export default function () { + if (usedOnce === false) { + usedOnce = true + return "Something went wrong" + } + + dynamicMessage += "+" + + return dynamicMessage +} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/404.astro b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/404.astro new file mode 100644 index 000000000..37fd1c1d3 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/404.astro @@ -0,0 +1,5 @@ +--- +import message from "../nondeterminism-404" +export const prerender = true; +--- +{message()} diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/500.astro b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/500.astro new file mode 100644 index 000000000..ef91ad0ff --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/500.astro @@ -0,0 +1,6 @@ +--- +import "../external-stylesheet.css" +import message from "../nondeterminism-500" +export const prerender = true +--- +<h1>{message()}</h1> diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/fivehundred.astro b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/fivehundred.astro new file mode 100644 index 000000000..99d103567 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/fivehundred.astro @@ -0,0 +1,4 @@ +--- +return new Response(null, { status: 500 }) +--- +<p>This html will not be served</p> diff --git a/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/static.astro b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/static.astro new file mode 100644 index 000000000..af6bad2fb --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender-404-500/src/pages/static.astro @@ -0,0 +1,12 @@ +--- +export const prerender = true; +--- + +<html> +<head> + <title>Static Page</title> +</head> + <body> + <h1>Hello world!</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/prerender/package.json b/packages/integrations/node/test/fixtures/prerender/package.json new file mode 100644 index 000000000..f0e873151 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-prerender", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/prerender/src/middleware.ts b/packages/integrations/node/test/fixtures/prerender/src/middleware.ts new file mode 100644 index 000000000..3083acd24 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/middleware.ts @@ -0,0 +1,5 @@ +import { shared } from './shared'; +export const onRequest = (ctx, next) => { + ctx.locals.name = shared; + return next(); +}; diff --git a/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro b/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro new file mode 100644 index 000000000..f3a26721d --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/pages/one.astro @@ -0,0 +1,10 @@ +--- +--- +<html> + <head> + <title>One</title> + </head> + <body> + <h1>One</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/prerender/src/pages/third.astro b/packages/integrations/node/test/fixtures/prerender/src/pages/third.astro new file mode 100644 index 000000000..e29377d88 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/pages/third.astro @@ -0,0 +1,15 @@ +--- +import { shared} from "../shared"; +export const prerender = false; + +const shared = Astro.locals.name; +--- + +<html> +<head> + <title>One</title> +</head> +<body> +<h1>{shared}</h1> +</body> +</html> diff --git a/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro b/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro new file mode 100644 index 000000000..beb6e8d78 --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/pages/two.astro @@ -0,0 +1,11 @@ +--- +export const prerender = true; +--- +<html> + <head> + <title>Two</title> + </head> + <body> + <h1>Two</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/prerender/src/shared.ts b/packages/integrations/node/test/fixtures/prerender/src/shared.ts new file mode 100644 index 000000000..cd35843de --- /dev/null +++ b/packages/integrations/node/test/fixtures/prerender/src/shared.ts @@ -0,0 +1 @@ +export const shared = 'shared'; diff --git a/packages/integrations/node/test/fixtures/preview-headers/package.json b/packages/integrations/node/test/fixtures/preview-headers/package.json new file mode 100644 index 000000000..0764f2581 --- /dev/null +++ b/packages/integrations/node/test/fixtures/preview-headers/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/nodejs-preview-headers", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/preview-headers/src/pages/index.astro b/packages/integrations/node/test/fixtures/preview-headers/src/pages/index.astro new file mode 100644 index 000000000..10ddd6d25 --- /dev/null +++ b/packages/integrations/node/test/fixtures/preview-headers/src/pages/index.astro @@ -0,0 +1 @@ +Hello! diff --git a/packages/integrations/node/test/fixtures/trailing-slash/astro.config.mjs b/packages/integrations/node/test/fixtures/trailing-slash/astro.config.mjs new file mode 100644 index 000000000..acf78132b --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/astro.config.mjs @@ -0,0 +1,8 @@ +import node from '@astrojs/node' + +export default { + base: '/some-base', + output: 'static', + trailingSlash: 'never', + adapter: node({ mode: 'standalone' }) +}; diff --git a/packages/integrations/node/test/fixtures/trailing-slash/package.json b/packages/integrations/node/test/fixtures/trailing-slash/package.json new file mode 100644 index 000000000..69a1b92c4 --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/node-trailingslash", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/trailing-slash/public/one.css b/packages/integrations/node/test/fixtures/trailing-slash/public/one.css new file mode 100644 index 000000000..5ce768ca5 --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/public/one.css @@ -0,0 +1 @@ +h1 { color: red; } diff --git a/packages/integrations/node/test/fixtures/trailing-slash/src/pages/index.astro b/packages/integrations/node/test/fixtures/trailing-slash/src/pages/index.astro new file mode 100644 index 000000000..a4c415519 --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/src/pages/index.astro @@ -0,0 +1,8 @@ +<html> + <head> + <title>Index</title> + </head> + <body> + <h1>Index</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/trailing-slash/src/pages/one.astro b/packages/integrations/node/test/fixtures/trailing-slash/src/pages/one.astro new file mode 100644 index 000000000..aa370d18d --- /dev/null +++ b/packages/integrations/node/test/fixtures/trailing-slash/src/pages/one.astro @@ -0,0 +1,11 @@ +--- +export const prerender = true; +--- +<html> + <head> + <title>One</title> + </head> + <body> + <h1>One</h1> + </body> +</html> diff --git a/packages/integrations/node/test/fixtures/url/package.json b/packages/integrations/node/test/fixtures/url/package.json new file mode 100644 index 000000000..07145a8cb --- /dev/null +++ b/packages/integrations/node/test/fixtures/url/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/url", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/url/src/pages/index.astro b/packages/integrations/node/test/fixtures/url/src/pages/index.astro new file mode 100644 index 000000000..003429f52 --- /dev/null +++ b/packages/integrations/node/test/fixtures/url/src/pages/index.astro @@ -0,0 +1,9 @@ +--- +--- + +<html lang="en"> + <head> + <title>URL</title> + </head> + <body>{Astro.url.href}</body> +</html> diff --git a/packages/integrations/node/test/fixtures/well-known-locations/package.json b/packages/integrations/node/test/fixtures/well-known-locations/package.json new file mode 100644 index 000000000..3a8a66de7 --- /dev/null +++ b/packages/integrations/node/test/fixtures/well-known-locations/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/well-known-locations", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "^5.1.6" + } +} diff --git a/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden/file.json b/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden/file.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/integrations/node/test/fixtures/well-known-locations/public/.hidden/file.json @@ -0,0 +1 @@ +{} diff --git a/packages/integrations/node/test/fixtures/well-known-locations/public/.well-known/apple-app-site-association b/packages/integrations/node/test/fixtures/well-known-locations/public/.well-known/apple-app-site-association new file mode 100644 index 000000000..daae260f1 --- /dev/null +++ b/packages/integrations/node/test/fixtures/well-known-locations/public/.well-known/apple-app-site-association @@ -0,0 +1,3 @@ +{ + "applinks": {} +} diff --git a/packages/integrations/node/test/headers.test.js b/packages/integrations/node/test/headers.test.js new file mode 100644 index 000000000..f2753517e --- /dev/null +++ b/packages/integrations/node/test/headers.test.js @@ -0,0 +1,148 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('Node Adapter Headers', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/headers/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + }); + + it('Endpoint Simple Headers', async () => { + await runTest('/endpoints/simple', { + 'content-type': 'text/plain;charset=utf-8', + 'x-hello': 'world', + }); + }); + + it('Endpoint Astro Single Cookie Header', async () => { + await runTest('/endpoints/astro-cookies-single', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': 'from1=astro1', + }); + }); + + it('Endpoint Astro Multi Cookie Header', async () => { + await runTest('/endpoints/astro-cookies-multi', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': ['from1=astro1', 'from2=astro2'], + }); + }); + + it('Endpoint Response Single Cookie Header', async () => { + await runTest('/endpoints/response-cookies-single', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': 'hello1=world1', + }); + }); + + it('Endpoint Response Multi Cookie Header', async () => { + await runTest('/endpoints/response-cookies-multi', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': ['hello1=world1', 'hello2=world2'], + }); + }); + + it('Endpoint Complex Headers Kitchen Sink', async () => { + await runTest('/endpoints/kitchen-sink', { + 'content-type': 'text/plain;charset=utf-8', + 'x-single': 'single', + 'x-triple': 'one, two, three', + 'set-cookie': ['hello1=world1', 'hello2=world2'], + }); + }); + + it('Endpoint Astro and Response Single Cookie Header', async () => { + await runTest('/endpoints/astro-response-cookie-single', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': ['from1=response1', 'from1=astro1'], + }); + }); + + it('Endpoint Astro and Response Multi Cookie Header', async () => { + await runTest('/endpoints/astro-response-cookie-multi', { + 'content-type': 'text/plain;charset=utf-8', + 'set-cookie': ['from1=response1', 'from2=response2', 'from3=astro1', 'from4=astro2'], + }); + }); + + it('Endpoint Response Empty Headers Object', async () => { + await runTest('/endpoints/response-empty-headers-object', { + 'content-type': 'text/plain;charset=UTF-8', + }); + }); + + it('Endpoint Response undefined Headers Object', async () => { + await runTest('/endpoints/response-undefined-headers-object', { + 'content-type': 'text/plain;charset=UTF-8', + }); + }); + + it('Component Astro Single Cookie Header', async () => { + await runTest('/astro/component-astro-cookies-single', { + 'content-type': 'text/html', + 'set-cookie': 'from1=astro1', + }); + }); + + it('Component Astro Multi Cookie Header', async () => { + await runTest('/astro/component-astro-cookies-multi', { + 'content-type': 'text/html', + 'set-cookie': ['from1=astro1', 'from2=astro2'], + }); + }); + + it('Component Response Single Cookie Header', async () => { + await runTest('/astro/component-response-cookies-single', { + 'content-type': 'text/html', + 'set-cookie': 'from1=value1', + }); + }); + + it('Component Response Multi Cookie Header', async () => { + await runTest('/astro/component-response-cookies-multi', { + 'content-type': 'text/html', + 'set-cookie': ['from1=value1', 'from2=value2'], + }); + }); + + it('Component Astro and Response Single Cookie Header', async () => { + await runTest('/astro/component-astro-response-cookie-single', { + 'content-type': 'text/html', + 'set-cookie': ['from1=response1', 'from1=astro1'], + }); + }); + + it('Component Astro and Response Multi Cookie Header', async () => { + await runTest('/astro/component-astro-response-cookie-multi', { + 'content-type': 'text/html', + 'set-cookie': ['from1=response1', 'from2=response2', 'from3=astro1', 'from4=astro2'], + }); + }); +}); + +async function runTest(url, expectedHeaders) { + const { handler } = await import('./fixtures/headers/dist/server/entry.mjs'); + + const { req, res, done } = createRequestAndResponse({ + method: 'GET', + url, + }); + + handler(req, res); + + req.send(); + + await done; + const headers = res.getHeaders(); + + assert.deepEqual(headers, expectedHeaders); +} diff --git a/packages/integrations/node/test/image.test.js b/packages/integrations/node/test/image.test.js new file mode 100644 index 000000000..ecd99a9ac --- /dev/null +++ b/packages/integrations/node/test/image.test.js @@ -0,0 +1,54 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { inferRemoteSize } from 'astro/assets/utils/inferRemoteSize.js'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture } from './test-utils.js'; + +describe('Image endpoint', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devPreview; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/image/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + image: { + domains: ['images.unsplash.com'], + }, + }); + await fixture.build(); + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + it('it returns local images', async () => { + const res = await fixture.fetch('/'); + assert.equal(res.status, 200); + const html = await res.text(); + const $ = cheerio.load(html); + + const img = $('img[alt=Penguins]').attr('src'); + const size = await inferRemoteSize(`http://localhost:4321${img}`); + assert.equal(size.format, 'webp'); + assert.equal(size.width, 50); + assert.equal(size.height, 33); + }); + + it('it returns remote images', async () => { + const res = await fixture.fetch('/'); + assert.equal(res.status, 200); + const html = await res.text(); + const $ = cheerio.load(html); + const img = $('img[alt=Cornwall]').attr('src'); + const size = await inferRemoteSize(`http://localhost:4321${img}`); + assert.equal(size.format, 'webp'); + assert.equal(size.width, 400); + assert.equal(size.height, 300); + }); +}); diff --git a/packages/integrations/node/test/locals.test.js b/packages/integrations/node/test/locals.test.js new file mode 100644 index 000000000..b8e3ed40f --- /dev/null +++ b/packages/integrations/node/test/locals.test.js @@ -0,0 +1,81 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('API routes', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/locals/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + }); + + it('Can use locals added by node middleware', async () => { + const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + url: '/from-node-middleware', + }); + + const locals = { foo: 'bar' }; + + handler(req, res, () => {}, locals); + req.send(); + + const html = await text(); + + assert.equal(html.includes('<h1>bar</h1>'), true); + }); + + it('Throws an error when provided non-objects as locals', async () => { + const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + url: '/from-node-middleware', + }); + + handler(req, res, undefined, 'locals'); + req.send(); + + await done; + assert.equal(res.statusCode, 500); + }); + + it('Can use locals added by astro middleware', async () => { + const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + + const { req, res, text } = createRequestAndResponse({ + url: '/from-astro-middleware', + }); + + handler(req, res, () => {}); + req.send(); + + const html = await text(); + + assert.equal(html.includes('<h1>baz</h1>'), true); + }); + + it('Can access locals in API', async () => { + const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const { req, res, done } = createRequestAndResponse({ + method: 'POST', + url: '/api', + }); + + const locals = { foo: 'bar' }; + + handler(req, res, () => {}, locals); + req.send(); + + const [buffer] = await done; + + const json = JSON.parse(buffer.toString('utf-8')); + + assert.equal(json.foo, 'bar'); + }); +}); diff --git a/packages/integrations/node/test/node-middleware.test.js b/packages/integrations/node/test/node-middleware.test.js new file mode 100644 index 000000000..8bc0b2777 --- /dev/null +++ b/packages/integrations/node/test/node-middleware.test.js @@ -0,0 +1,90 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import express from 'express'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +describe('behavior from middleware, standalone', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + root: './fixtures/node-middleware/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + describe('404', async () => { + it('when mode is standalone', async () => { + const res = await fetch(`http://${server.host}:${server.port}/error-page`); + + assert.equal(res.status, 404); + + const html = await res.text(); + const $ = cheerio.load(html); + + const body = $('body'); + assert.equal(body.text().includes('Page does not exist'), true); + }); + }); +}); + +describe('behavior from middleware, middleware', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + root: './fixtures/node-middleware/', + output: 'server', + adapter: nodejs({ mode: 'middleware' }), + }); + await fixture.build(); + const { handler } = await fixture.loadAdapterEntryModule(); + const app = express(); + app.use(handler); + server = app.listen(8889); + }); + + after(async () => { + server.close(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('when mode is middleware', async () => { + const res = await fetch('http://localhost:8889/ssr'); + + assert.equal(res.status, 200); + + const html = await res.text(); + const $ = cheerio.load(html); + + const body = $('body'); + assert.equal(body.text().includes("Here's a random number"), true); + }); +}); diff --git a/packages/integrations/node/test/prerender-404-500.test.js b/packages/integrations/node/test/prerender-404-500.test.js new file mode 100644 index 000000000..a7e968f0c --- /dev/null +++ b/packages/integrations/node/test/prerender-404-500.test.js @@ -0,0 +1,284 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +describe('Prerender 404', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + describe('With base', async () => { + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.dev/', + base: '/some-base', + root: './fixtures/prerender-404-500/', + output: 'server', + outDir: './dist/server-with-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + process.env.PRERENDER = undefined; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/static`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Hello world!'); + }); + + it('Can handle prerendered 404', async () => { + const url = `http://${server.host}:${server.port}/some-base/missing`; + const res1 = await fetch(url); + const res2 = await fetch(url); + const res3 = await fetch(url); + + assert.equal(res1.status, 404); + assert.equal(res2.status, 404); + assert.equal(res3.status, 404); + + const html1 = await res1.text(); + const html2 = await res2.text(); + const html3 = await res3.text(); + + assert.equal(html1, html2); + assert.equal(html2, html3); + + const $ = cheerio.load(html1); + + assert.equal($('body').text(), 'Page does not exist'); + }); + + it(' Can handle prerendered 500 called indirectly', async () => { + const url = `http://${server.host}:${server.port}/some-base/fivehundred`; + const response1 = await fetch(url); + const response2 = await fetch(url); + const response3 = await fetch(url); + + assert.equal(response1.status, 500); + + const html1 = await response1.text(); + const html2 = await response2.text(); + const html3 = await response3.text(); + + assert.equal(html1.includes('Something went wrong'), true); + + assert.equal(html1, html2); + assert.equal(html2, html3); + }); + + it('prerendered 500 page includes expected styles', async () => { + const response = await fetch(`http://${server.host}:${server.port}/some-base/fivehundred`); + const html = await response.text(); + const $ = cheerio.load(html); + + // length will be 0 if the stylesheet does not get included + assert.equal($('style').length, 1); + }); + }); + + describe('Without base', async () => { + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.info/', + root: './fixtures/prerender-404-500/', + output: 'server', + outDir: './dist/server-without-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + process.env.PRERENDER = undefined; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/static`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Hello world!'); + }); + + it('Can handle prerendered 404', async () => { + const url = `http://${server.host}:${server.port}/some-base/missing`; + const res1 = await fetch(url); + const res2 = await fetch(url); + const res3 = await fetch(url); + + assert.equal(res1.status, 404); + assert.equal(res2.status, 404); + assert.equal(res3.status, 404); + + const html1 = await res1.text(); + const html2 = await res2.text(); + const html3 = await res3.text(); + + assert.equal(html1, html2); + assert.equal(html2, html3); + + const $ = cheerio.load(html1); + + assert.equal($('body').text(), 'Page does not exist'); + }); + }); +}); + +describe('Hybrid 404', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + describe('With base', async () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.com/', + base: '/some-base', + root: './fixtures/prerender-404-500/', + output: 'static', + outDir: './dist/hybrid-with-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + process.env.PRERENDER = undefined; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/static`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Hello world!'); + }); + + it('Can handle prerendered 404', async () => { + const url = `http://${server.host}:${server.port}/some-base/missing`; + const res1 = await fetch(url); + const res2 = await fetch(url); + const res3 = await fetch(url); + + assert.equal(res1.status, 404); + assert.equal(res2.status, 404); + assert.equal(res3.status, 404); + + const html1 = await res1.text(); + const html2 = await res2.text(); + const html3 = await res3.text(); + + assert.equal(html1, html2); + assert.equal(html2, html3); + + const $ = cheerio.load(html1); + + assert.equal($('body').text(), 'Page does not exist'); + }); + }); + + describe('Without base', async () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + // inconsequential config that differs between tests + // to bust cache and prevent modules and their state + // from being reused + site: 'https://test.net/', + root: './fixtures/prerender-404-500/', + output: 'static', + outDir: './dist/hybrid-without-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + process.env.PRERENDER = undefined; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/static`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Hello world!'); + }); + + it('Can handle prerendered 404', async () => { + const url = `http://${server.host}:${server.port}/missing`; + const res1 = await fetch(url); + const res2 = await fetch(url); + const res3 = await fetch(url); + + assert.equal(res1.status, 404); + assert.equal(res2.status, 404); + assert.equal(res3.status, 404); + + const html1 = await res1.text(); + const html2 = await res2.text(); + const html3 = await res3.text(); + + assert.equal(html1, html2); + assert.equal(html2, html3); + + const $ = cheerio.load(html1); + + assert.equal($('body').text(), 'Page does not exist'); + }); + }); +}); diff --git a/packages/integrations/node/test/prerender.test.js b/packages/integrations/node/test/prerender.test.js new file mode 100644 index 000000000..71137b76a --- /dev/null +++ b/packages/integrations/node/test/prerender.test.js @@ -0,0 +1,419 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +describe('Prerendering', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + describe('With base', async () => { + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + base: '/some-base', + root: './fixtures/prerender/', + output: 'server', + outDir: './dist/with-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + assert.ok(fixture.pathExists('/client/two/index.html')); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route without trailing slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + }); + + describe('Without base', async () => { + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'server', + outDir: './dist/without-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + assert.ok(fixture.pathExists('/client/two/index.html')); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + }); + + describe('Via integration', () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'server', + outDir: './dist/via-integration', + adapter: nodejs({ mode: 'standalone' }), + integrations: [ + { + name: 'test', + hooks: { + 'astro:route:setup': ({ route }) => { + if (route.component.endsWith('two.astro')) { + route.prerender = true; + } + }, + }, + }, + ], + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + assert.ok(fixture.pathExists('/client/two/index.html')); + }); + }); + + describe('Dev', () => { + let devServer; + + before(async () => { + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'server', + outDir: './dist/dev', + adapter: nodejs({ mode: 'standalone' }), + }); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation> + const res = await fixture.fetch(`/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route', async () => { + // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation> + const res = await fixture.fetch(`/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + }); +}); + +describe('Hybrid rendering', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + describe('With base', () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + base: '/some-base', + root: './fixtures/prerender/', + output: 'static', + outDir: './dist/hybrid-with-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/two`); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + assert.ok(fixture.pathExists('/client/one/index.html')); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without trailing slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + + describe('Without base', () => { + before(async () => { + process.env.PRERENDER = false; + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'static', + outDir: './dist/hybrid-without-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/two`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Two'); + }); + + it('Can render prerendered route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + assert.ok(fixture.pathExists('/client/one/index.html')); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + + describe('Shared modules', () => { + before(async () => { + process.env.PRERENDER = false; + + fixture = await loadFixture({ + root: './fixtures/prerender/', + output: 'static', + outDir: './dist/hybrid-shared-modules', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render SSR route', async () => { + const res = await fetch(`http://${server.host}:${server.port}/third`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'shared'); + }); + }); +}); diff --git a/packages/integrations/node/test/preview-headers.test.js b/packages/integrations/node/test/preview-headers.test.js new file mode 100644 index 000000000..3fd9d0508 --- /dev/null +++ b/packages/integrations/node/test/preview-headers.test.js @@ -0,0 +1,38 @@ +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'; + +describe('Astro preview headers', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let devPreview; + const headers = { + astro: 'test', + }; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/preview-headers/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + server: { + headers, + }, + }); + await fixture.build(); + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + describe('Preview Headers', () => { + it('returns custom headers for valid URLs', async () => { + const result = await fixture.fetch('/'); + assert.equal(result.status, 200); + assert.equal(Object.fromEntries(result.headers).astro, headers.astro); + }); + }); +}); diff --git a/packages/integrations/node/test/preview-host.test.js b/packages/integrations/node/test/preview-host.test.js new file mode 100644 index 000000000..44dde0837 --- /dev/null +++ b/packages/integrations/node/test/preview-host.test.js @@ -0,0 +1,63 @@ +import 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'; + +describe('Astro preview host', () => { + it('defaults to localhost', async () => { + const fixture = await loadFixture({ + root: './fixtures/preview-headers/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const devPreview = await fixture.preview(); + assert.equal(devPreview.host, 'localhost'); + await devPreview.stop(); + }); + + it('uses default when set to false', async () => { + const fixture = await loadFixture({ + root: './fixtures/preview-headers/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + server: { + host: false, + }, + }); + await fixture.build(); + const devPreview = await fixture.preview(); + assert.equal(devPreview.host, 'localhost'); + await devPreview.stop(); + }); + + it('sets wildcard host if set to true', async () => { + const fixture = await loadFixture({ + root: './fixtures/preview-headers/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + server: { + host: true, + }, + }); + await fixture.build(); + const devPreview = await fixture.preview(); + assert.equal(devPreview.host, '0.0.0.0'); + await devPreview.stop(); + }); + + it('allows setting specific host', async () => { + const fixture = await loadFixture({ + root: './fixtures/preview-headers/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + server: { + host: '127.0.0.1', + }, + }); + await fixture.build(); + const devPreview = await fixture.preview(); + assert.equal(devPreview.host, '127.0.0.1'); + await devPreview.stop(); + }); +}); diff --git a/packages/integrations/node/test/server-host.test.js b/packages/integrations/node/test/server-host.test.js new file mode 100644 index 000000000..facd32d47 --- /dev/null +++ b/packages/integrations/node/test/server-host.test.js @@ -0,0 +1,21 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { hostOptions } from '../dist/standalone.js'; + +describe('host', () => { + it('returns "0.0.0.0" when host is true', () => { + const options = { host: true }; + assert.equal(hostOptions(options.host), '0.0.0.0'); + }); + + it('returns "localhost" when host is false', () => { + const options = { host: false }; + assert.equal(hostOptions(options.host), 'localhost'); + }); + + it('returns the value of host when host is a string', () => { + const host = '1.1.1.1'; + const options = { host }; + assert.equal(hostOptions(options.host), host); + }); +}); diff --git a/packages/integrations/node/test/test-utils.js b/packages/integrations/node/test/test-utils.js new file mode 100644 index 000000000..37389d6d7 --- /dev/null +++ b/packages/integrations/node/test/test-utils.js @@ -0,0 +1,82 @@ +import { EventEmitter } from 'node:events'; +import { loadFixture as baseLoadFixture } from '@astrojs/test-utils'; +import httpMocks from 'node-mocks-http'; + +process.env.ASTRO_NODE_AUTOSTART = 'disabled'; +process.env.ASTRO_NODE_LOGGING = 'disabled'; +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +export function loadFixture(inlineConfig) { + if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); + + // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath + // without this, the main `loadFixture` helper will resolve relative to `packages/astro/test` + return baseLoadFixture({ + ...inlineConfig, + root: new URL(inlineConfig.root, import.meta.url).toString(), + }); +} + +export function createRequestAndResponse(reqOptions) { + const req = httpMocks.createRequest(reqOptions); + + const res = httpMocks.createResponse({ + eventEmitter: EventEmitter, + req, + }); + + const done = toPromise(res); + + // Get the response as text + const text = async () => { + const chunks = await done; + return buffersToString(chunks); + }; + + return { req, res, done, text }; +} + +export function toPromise(res) { + return new Promise((resolve) => { + // node-mocks-http doesn't correctly handle non-Buffer typed arrays, + // so override the write method to fix it. + const write = res.write; + res.write = function (data, encoding) { + if (ArrayBuffer.isView(data) && !Buffer.isBuffer(data)) { + // biome-ignore lint/style/noParameterAssign: <explanation> + data = Buffer.from(data.buffer); + } + return write.call(this, data, encoding); + }; + res.on('end', () => { + const chunks = res._getChunks(); + resolve(chunks); + }); + }); +} + +export function buffersToString(buffers) { + const decoder = new TextDecoder(); + let str = ''; + for (const buffer of buffers) { + str += decoder.decode(buffer); + } + return str; +} + +export function waitServerListen(server) { + return new Promise((resolve, reject) => { + function onListen() { + server.off('error', onError); + resolve(); + } + function onError(error) { + server.off('listening', onListen); + reject(error); + } + server.once('listening', onListen); + server.once('error', onError); + }); +} diff --git a/packages/integrations/node/test/trailing-slash.test.js b/packages/integrations/node/test/trailing-slash.test.js new file mode 100644 index 000000000..2a73efa75 --- /dev/null +++ b/packages/integrations/node/test/trailing-slash.test.js @@ -0,0 +1,434 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +/** + * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture + */ + +describe('Trailing slash', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + describe('Always', async () => { + describe('With base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + base: '/some-base', + output: 'static', + trailingSlash: 'always', + outDir: './dist/always-with-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with redirect', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/some-base/one/'); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one?foo=bar`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/some-base/one/?foo=bar'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Does not add trailing slash to subresource urls', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one.css`); + const css = await res.text(); + + assert.equal(res.status, 200); + assert.equal(css, 'h1 { color: red; }\n'); + }); + }); + describe('Without base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + output: 'static', + trailingSlash: 'always', + outDir: './dist/always-without-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with redirect', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/one/'); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one?foo=bar`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/one/?foo=bar'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Does not add trailing slash to subresource urls', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one.css`); + const css = await res.text(); + + assert.equal(res.status, 200); + assert.equal(css, 'h1 { color: red; }\n'); + }); + }); + }); + describe('Never', async () => { + describe('With base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + base: '/some-base', + output: 'static', + trailingSlash: 'never', + outDir: './dist/never-with-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with redirect', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/some-base/one'); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`, { + redirect: 'manual', + }); + + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/some-base/one?foo=bar'); + }); + + it('Can render prerendered route with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + describe('Without base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + output: 'static', + trailingSlash: 'never', + outDir: './dist/never-without-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with redirect', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/`, { + redirect: 'manual', + }); + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/one'); + }); + + it('Can render prerendered route with redirect and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`, { + redirect: 'manual', + }); + + assert.equal(res.status, 301); + assert.equal(res.headers.get('location'), '/one?foo=bar'); + }); + + it('Can render prerendered route and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + }); + describe('Ignore', async () => { + describe('With base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + base: '/some-base', + output: 'static', + trailingSlash: 'ignore', + outDir: './dist/ignore-with-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route with slash and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one/?foo=bar`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without slash and with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/some-base/one?foo=bar`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + describe('Without base', async () => { + before(async () => { + process.env.ASTRO_NODE_AUTOSTART = 'disabled'; + process.env.PRERENDER = true; + + fixture = await loadFixture({ + root: './fixtures/trailing-slash/', + output: 'static', + trailingSlash: 'ignore', + outDir: './dist/ignore-without-base', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + // biome-ignore lint/performance/noDelete: <explanation> + delete process.env.PRERENDER; + }); + + it('Can render prerendered base route', async () => { + const res = await fetch(`http://${server.host}:${server.port}`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'Index'); + }); + + it('Can render prerendered route with slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without slash', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route with slash and query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one/?foo=bar`, { + redirect: 'manual', + }); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + + it('Can render prerendered route without slash and with query params', async () => { + const res = await fetch(`http://${server.host}:${server.port}/one?foo=bar`); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal(res.status, 200); + assert.equal($('h1').text(), 'One'); + }); + }); + }); +}); diff --git a/packages/integrations/node/test/url.test.js b/packages/integrations/node/test/url.test.js new file mode 100644 index 000000000..81b357b71 --- /dev/null +++ b/packages/integrations/node/test/url.test.js @@ -0,0 +1,115 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { TLSSocket } from 'node:tls'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { createRequestAndResponse, loadFixture } from './test-utils.js'; + +describe('URL', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/url/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + }); + + it('return http when non-secure', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('http:'), true); + }); + + it('return https when secure', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + socket: new TLSSocket(), + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('https:'), true); + }); + + it('return http when the X-Forwarded-Proto header is set to http', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + headers: { 'X-Forwarded-Proto': 'http' }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('http:'), true); + }); + + it('return https when the X-Forwarded-Proto header is set to https', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + headers: { 'X-Forwarded-Proto': 'https' }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + assert.equal(html.includes('https:'), true); + }); + + it('includes forwarded host and port in the url', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + headers: { + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Host': 'abc.xyz', + 'X-Forwarded-Port': '444', + }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + const $ = cheerio.load(html); + + assert.equal($('body').text(), 'https://abc.xyz:444/'); + }); + + it('accepts port in forwarded host and forwarded port', async () => { + const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const { req, res, text } = createRequestAndResponse({ + headers: { + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Host': 'abc.xyz:444', + 'X-Forwarded-Port': '444', + }, + url: '/', + }); + + handler(req, res); + req.send(); + + const html = await text(); + const $ = cheerio.load(html); + + assert.equal($('body').text(), 'https://abc.xyz:444/'); + }); +}); diff --git a/packages/integrations/node/test/well-known-locations.test.js b/packages/integrations/node/test/well-known-locations.test.js new file mode 100644 index 000000000..0951d6c27 --- /dev/null +++ b/packages/integrations/node/test/well-known-locations.test.js @@ -0,0 +1,46 @@ +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'; + +describe('test URIs beginning with a dot', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/well-known-locations/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + }); + + describe('can load well-known URIs', async () => { + let devPreview; + + before(async () => { + devPreview = await fixture.preview(); + }); + + after(async () => { + await devPreview.stop(); + }); + + it('can load a valid well-known URI', async () => { + const res = await fixture.fetch('/.well-known/apple-app-site-association'); + + assert.equal(res.status, 200); + + const json = await res.json(); + + assert.notEqual(json.applinks, {}); + }); + + it('cannot load a dot folder that is not a well-known URI', async () => { + const res = await fixture.fetch('/.hidden/file.json'); + + assert.equal(res.status, 404); + }); + }); +}); diff --git a/packages/integrations/node/tsconfig.json b/packages/integrations/node/tsconfig.json new file mode 100644 index 000000000..18443cddf --- /dev/null +++ b/packages/integrations/node/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "outDir": "./dist" + } +} |