Compare commits

...

21 Commits

Author SHA1 Message Date
Vercel Release Bot
6476f4f786 Version Packages (#11551)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @vercel/functions@1.0.0

### Major Changes

- Initial release
([#11553](https://github.com/vercel/vercel/pull/11553))

## @vercel/node@3.1.0

### Minor Changes

- Make waitUntil consistent for Node.js & Edge
([#11553](https://github.com/vercel/vercel/pull/11553))

## vercel@34.1.9

### Patch Changes

- Updated dependencies
\[[`5a532a5b9`](5a532a5b94),
[`50fc27ba5`](50fc27ba57),
[`c1d852295`](c1d8522950),
[`a5ea04154`](a5ea04154b)]:
    -   @vercel/next@4.2.8
    -   @vercel/node@3.1.0

## @vercel/next@4.2.8

### Patch Changes

- Fix missing initial RSC headers
([#11552](https://github.com/vercel/vercel/pull/11552))

- Remove .prefetch.rsc rewrites for non-PPR
([#11540](https://github.com/vercel/vercel/pull/11540))

- [next] rename middleware manifest env
([#11549](https://github.com/vercel/vercel/pull/11549))

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-07 15:40:37 +02:00
Kiko Beats
a5ea04154b Make waitUntil consistent for Node.js & Edge (#11553)
This PR makes `waitUntil` consistent when interacting with it via `vc
dev`. That includes:

**be possible to call `waitUntil` from the context second argument in
web handlers functions (Node.js & Edge)**:

```js
export function GET(request, { waitUntil }) {
  waitUntil(fetch('https://vercel.com'));
  return new Response('OK');
}
```

**be possible to call `waitUntil` imported from `@vercel/functions`**
(Node.js & Edge):

```js
import { waitUntil } from '@vercel/functions';

export function GET(request) {
  waitUntil(fetch('https://vercel.com'));
  return new Response('OK');
}
```

Additionally, `@vercel/functions` can be adopted for other framework to
take advantage of this pattern at Vercel.

---------

Co-authored-by: Javi Velasco <javier.velasco86@gmail.com>
2024-05-07 15:36:43 +02:00
JJ Kasper
5a532a5b94 [next] Fix missing initial RSC headers (#11552)
While investigating https://github.com/vercel/next.js/issues/59883 we noticed the RSC Prerender did not have all the expected headers like the HTML Prerender which caused unexpected revalidation behavior specifically on client-transition as some of these headers are used to detect if a revalidated tag is associated with specific outputs. 

x-ref: https://github.com/vercel/next.js/issues/59883
2024-05-07 07:43:00 +00:00
Jiachi Liu
c1d8522950 [next] rename middleware manifest env (#11549)
Rename `environment` property to `env` from middleware manifest added in
#11390.
Align with https://github.com/vercel/next.js/pull/64521

The `environments` property wasn't working properly before due to
unfinished next.js changes, it should work now after testing with next
build from https://github.com/vercel/next.js/pull/64521. We change it to
a better naming word `env`.
2024-05-06 21:32:07 +02:00
JJ Kasper
50fc27ba57 [next] Remove .prefetch.rsc rewrites for non-PPR (#11540)
Removes .prefetch.rsc rewrites rules for non-PPR, as Next.js doesn't emit any static prefetches otherwise
2024-05-06 18:17:22 +00:00
Joel Henningsen
3101d2497f Fix broken link to Code of Conduct in README (#11544)
### Purpose of the Pull Request

This PR addresses a broken link in the README.md file that is supposed
to direct readers to the Code of Conduct. Previously, the link pointed
to `CODE_OF_CONDUCT.md`, which does not exist in the repository's main
directory. The corrected link points to the actual location of the file
in the `.github` directory.

### Changes Made

- Updated the link to the Code of Conduct from `[Code of
Conduct](CODE_OF_CONDUCT.md)` to `[Code of
Conduct](./.github/CODE_OF_CONDUCT.md)` to reflect the correct path.

### Additional Information

I previously opened discussion #11498 regarding this issue with the goal
of following the [Contribuing
Guidelines](https://github.com/vercel/vercel?tab=readme-ov-file#contributing),
but did not receive any feedback over the past week. Given the nature of
the change (a simple documentation fix), I proceeded with this pull
request to ensure the documentation remains useful and accurate for all
users.

### Impact

This change will help ensure that new contributors and users can easily
access the Code of Conduct, fostering better community interactions and
adherence to project standards.

Thank you for considering this pull request. I am open to any further
suggestions or requirements needed to merge this change.
2024-05-04 17:28:21 -07:00
Vercel Release Bot
4b0716b556 Version Packages (#11524)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## vercel@34.1.8

### Patch Changes

- Updated dependencies
\[[`52e435aa5`](52e435aa5d),
[`124846a3e`](124846a3e6),
[`dc974b679`](dc974b6797),
[`58c6755e0`](58c6755e0c)]:
    -   @vercel/next@4.2.7
    -   @vercel/static-build@2.5.3

## @vercel/next@4.2.7

### Patch Changes

- Fix missing .rsc outputs for pages prerenders
([#11503](https://github.com/vercel/vercel/pull/11503))

- Fix base path app index RSC handling
([#11528](https://github.com/vercel/vercel/pull/11528))

- Fix interception routes PPR route handling
([#11527](https://github.com/vercel/vercel/pull/11527))

## @vercel/static-build@2.5.3

### Patch Changes

- Support Ruby and Python static site generators in AL2023 build image
([#11529](https://github.com/vercel/vercel/pull/11529))

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-03 10:01:03 -07:00
JJ Kasper
124846a3e6 [next] Fix base path app index RSC handling (#11528)
This ensures we properly name the outputs when a basePath is present for the `index` output as previously we weren't properly routing to the RSC output due to nested `index` naming/route conflict. This only was a noticeable issue with prerender/static index outputs with base path and app router since if the index route was dynamic it would still catch the RSC request and just handle it without the correct naming. 

Fixes: https://github.com/vercel/next.js/issues/64903
2024-05-02 21:07:58 +00:00
dependabot[bot]
30cd0ec898 [framework-fixtures]: Bump the core group in /packages/static-build/test/fixtures/ionic-angular-v8 with 2 updates (#11530)
Bumps the core group in /packages/static-build/test/fixtures/ionic-angular-v8 with 2 updates: [@ionic/angular](https://github.com/ionic-team/ionic-framework) and [@ionic/core](https://github.com/ionic-team/ionic-framework).

Updates `@ionic/angular` from 8.0.1 to 8.1.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/ionic-team/ionic-framework/releases"><code>@​ionic/angular</code>'s releases</a>.</em></p>
<blockquote>
<h2>v8.1.0</h2>
<h1><a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.2...v8.1.0">8.1.0</a> (2024-05-01)</h1>
<h3>Features</h3>
<ul>
<li>add experimental transition focus manager (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29400">#29400</a>) (<a href="5b686efc10">5b686ef</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/23650">#23650</a></li>
<li><strong>content:</strong> add fixedSlotPlacement prop (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29233">#29233</a>) (<a href="90a7e70a1c">90a7e70</a>)</li>
<li><strong>datetime:</strong> pass roles to overlay when dismissing (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29221">#29221</a>) (<a href="6945adc3cc">6945adc</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/28298">#28298</a></li>
<li><strong>input:</strong> add clearInputIcon property (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29246">#29246</a>) (<a href="0f5d1c02d2">0f5d1c0</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/26974">#26974</a></li>
<li><strong>modal, popover:</strong> add ability to temporarily disable focus trapping (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29379">#29379</a>) (<a href="7c00351680">7c00351</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/24646">#24646</a></li>
<li><strong>picker:</strong> picker column is easier to select with assistive technology (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29371">#29371</a>) (<a href="e38e2e4d35">e38e2e4</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/25221">#25221</a></li>
</ul>
<h2>v8.0.2</h2>
<h2><a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.1...v8.0.2">8.0.2</a> (2024-05-01)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>datetime:</strong> navigating months in RTL works correctly (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29421">#29421</a>) (<a href="4d09890d69">4d09890</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29198">#29198</a></li>
<li><strong>ios:</strong> large title transition accounts for back button with no text  (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29327">#29327</a>) (<a href="bd8d065e1a">bd8d065</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/28751">#28751</a></li>
<li><strong>md:</strong> item border has improved contrast in dark mode (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29398">#29398</a>) (<a href="fa85f030cf">fa85f03</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29386">#29386</a></li>
<li><strong>select:</strong> options are visible with fit-content width and fill outline (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29408">#29408</a>) (<a href="f15b62a9ca">f15b62a</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29321">#29321</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/ionic-team/ionic-framework/blob/main/CHANGELOG.md"><code>@​ionic/angular</code>'s changelog</a>.</em></p>
<blockquote>
<h1><a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.2...v8.1.0">8.1.0</a> (2024-05-01)</h1>
<h3>Features</h3>
<ul>
<li>add experimental transition focus manager (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29400">#29400</a>) (<a href="5b686efc10">5b686ef</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/23650">#23650</a></li>
<li><strong>content:</strong> add fixedSlotPlacement prop (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29233">#29233</a>) (<a href="90a7e70a1c">90a7e70</a>)</li>
<li><strong>datetime:</strong> pass roles to overlay when dismissing (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29221">#29221</a>) (<a href="6945adc3cc">6945adc</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/28298">#28298</a></li>
<li><strong>input:</strong> add clearInputIcon property (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29246">#29246</a>) (<a href="0f5d1c02d2">0f5d1c0</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/26974">#26974</a></li>
<li><strong>modal, popover:</strong> add ability to temporarily disable focus trapping (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29379">#29379</a>) (<a href="7c00351680">7c00351</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/24646">#24646</a></li>
<li><strong>picker:</strong> picker column is easier to select with assistive technology (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29371">#29371</a>) (<a href="e38e2e4d35">e38e2e4</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/25221">#25221</a></li>
</ul>
<h2><a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.1...v8.0.2">8.0.2</a> (2024-05-01)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>datetime:</strong> navigating months in RTL works correctly (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29421">#29421</a>) (<a href="4d09890d69">4d09890</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29198">#29198</a></li>
<li><strong>ios:</strong> large title transition accounts for back button with no text  (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29327">#29327</a>) (<a href="bd8d065e1a">bd8d065</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/28751">#28751</a></li>
<li><strong>md:</strong> item border has improved contrast in dark mode (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29398">#29398</a>) (<a href="fa85f030cf">fa85f03</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29386">#29386</a></li>
<li><strong>select:</strong> options are visible with fit-content width and fill outline (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29408">#29408</a>) (<a href="f15b62a9ca">f15b62a</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29321">#29321</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="78d3076703"><code>78d3076</code></a> v8.1.0</li>
<li><a href="4ef5036c7b"><code>4ef5036</code></a> chore: sync with main (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29433">#29433</a>)</li>
<li><a href="72711bba2e"><code>72711bb</code></a> chore: sync with main</li>
<li><a href="d81c88403e"><code>d81c884</code></a> merge release-8.0.2 (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29431">#29431</a>)</li>
<li><a href="5b686efc10"><code>5b686ef</code></a> feat: add experimental transition focus manager (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29400">#29400</a>)</li>
<li><a href="665d67778f"><code>665d677</code></a> chore(): update package lock files</li>
<li><a href="101b50e14c"><code>101b50e</code></a> v8.0.2</li>
<li><a href="a01c3d49bb"><code>a01c3d4</code></a> docs(testing, components): clarify testing and focusable usage (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29424">#29424</a>)</li>
<li><a href="bd8d065e1a"><code>bd8d065</code></a> fix(ios): large title transition accounts for back button with no text  (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29327">#29327</a>)</li>
<li><a href="4d09890d69"><code>4d09890</code></a> fix(datetime): navigating months in RTL works correctly (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29421">#29421</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.1...v8.1.0">compare view</a></li>
</ul>
</details>
<br />

Updates `@ionic/core` from 8.0.1 to 8.1.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/ionic-team/ionic-framework/releases"><code>@​ionic/core</code>'s releases</a>.</em></p>
<blockquote>
<h2>v8.1.0</h2>
<h1><a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.2...v8.1.0">8.1.0</a> (2024-05-01)</h1>
<h3>Features</h3>
<ul>
<li>add experimental transition focus manager (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29400">#29400</a>) (<a href="5b686efc10">5b686ef</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/23650">#23650</a></li>
<li><strong>content:</strong> add fixedSlotPlacement prop (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29233">#29233</a>) (<a href="90a7e70a1c">90a7e70</a>)</li>
<li><strong>datetime:</strong> pass roles to overlay when dismissing (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29221">#29221</a>) (<a href="6945adc3cc">6945adc</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/28298">#28298</a></li>
<li><strong>input:</strong> add clearInputIcon property (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29246">#29246</a>) (<a href="0f5d1c02d2">0f5d1c0</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/26974">#26974</a></li>
<li><strong>modal, popover:</strong> add ability to temporarily disable focus trapping (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29379">#29379</a>) (<a href="7c00351680">7c00351</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/24646">#24646</a></li>
<li><strong>picker:</strong> picker column is easier to select with assistive technology (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29371">#29371</a>) (<a href="e38e2e4d35">e38e2e4</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/25221">#25221</a></li>
</ul>
<h2>v8.0.2</h2>
<h2><a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.1...v8.0.2">8.0.2</a> (2024-05-01)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>datetime:</strong> navigating months in RTL works correctly (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29421">#29421</a>) (<a href="4d09890d69">4d09890</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29198">#29198</a></li>
<li><strong>ios:</strong> large title transition accounts for back button with no text  (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29327">#29327</a>) (<a href="bd8d065e1a">bd8d065</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/28751">#28751</a></li>
<li><strong>md:</strong> item border has improved contrast in dark mode (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29398">#29398</a>) (<a href="fa85f030cf">fa85f03</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29386">#29386</a></li>
<li><strong>select:</strong> options are visible with fit-content width and fill outline (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29408">#29408</a>) (<a href="f15b62a9ca">f15b62a</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29321">#29321</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/ionic-team/ionic-framework/blob/main/CHANGELOG.md"><code>@​ionic/core</code>'s changelog</a>.</em></p>
<blockquote>
<h1><a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.2...v8.1.0">8.1.0</a> (2024-05-01)</h1>
<h3>Features</h3>
<ul>
<li>add experimental transition focus manager (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29400">#29400</a>) (<a href="5b686efc10">5b686ef</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/23650">#23650</a></li>
<li><strong>content:</strong> add fixedSlotPlacement prop (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29233">#29233</a>) (<a href="90a7e70a1c">90a7e70</a>)</li>
<li><strong>datetime:</strong> pass roles to overlay when dismissing (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29221">#29221</a>) (<a href="6945adc3cc">6945adc</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/28298">#28298</a></li>
<li><strong>input:</strong> add clearInputIcon property (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29246">#29246</a>) (<a href="0f5d1c02d2">0f5d1c0</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/26974">#26974</a></li>
<li><strong>modal, popover:</strong> add ability to temporarily disable focus trapping (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29379">#29379</a>) (<a href="7c00351680">7c00351</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/24646">#24646</a></li>
<li><strong>picker:</strong> picker column is easier to select with assistive technology (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29371">#29371</a>) (<a href="e38e2e4d35">e38e2e4</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/25221">#25221</a></li>
</ul>
<h2><a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.1...v8.0.2">8.0.2</a> (2024-05-01)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>datetime:</strong> navigating months in RTL works correctly (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29421">#29421</a>) (<a href="4d09890d69">4d09890</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29198">#29198</a></li>
<li><strong>ios:</strong> large title transition accounts for back button with no text  (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29327">#29327</a>) (<a href="bd8d065e1a">bd8d065</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/28751">#28751</a></li>
<li><strong>md:</strong> item border has improved contrast in dark mode (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29398">#29398</a>) (<a href="fa85f030cf">fa85f03</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29386">#29386</a></li>
<li><strong>select:</strong> options are visible with fit-content width and fill outline (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29408">#29408</a>) (<a href="f15b62a9ca">f15b62a</a>), closes <a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29321">#29321</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="78d3076703"><code>78d3076</code></a> v8.1.0</li>
<li><a href="4ef5036c7b"><code>4ef5036</code></a> chore: sync with main (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29433">#29433</a>)</li>
<li><a href="72711bba2e"><code>72711bb</code></a> chore: sync with main</li>
<li><a href="d81c88403e"><code>d81c884</code></a> merge release-8.0.2 (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29431">#29431</a>)</li>
<li><a href="5b686efc10"><code>5b686ef</code></a> feat: add experimental transition focus manager (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29400">#29400</a>)</li>
<li><a href="665d67778f"><code>665d677</code></a> chore(): update package lock files</li>
<li><a href="101b50e14c"><code>101b50e</code></a> v8.0.2</li>
<li><a href="a01c3d49bb"><code>a01c3d4</code></a> docs(testing, components): clarify testing and focusable usage (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29424">#29424</a>)</li>
<li><a href="bd8d065e1a"><code>bd8d065</code></a> fix(ios): large title transition accounts for back button with no text  (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29327">#29327</a>)</li>
<li><a href="4d09890d69"><code>4d09890</code></a> fix(datetime): navigating months in RTL works correctly (<a href="https://redirect.github.com/ionic-team/ionic-framework/issues/29421">#29421</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/ionic-team/ionic-framework/compare/v8.0.1...v8.1.0">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>
2024-05-02 15:23:12 +00:00
Nathan Rajlich
dc974b6797 [static-build] Support Ruby static site generators in AL2023 build image (#11529)
* Set `GEM_HOME` env var based on the Ruby version in `$PATH` (i.e.
`3.2.0` for the AL2 build image, `3.3.0` for the AL2023 build image)
* Copy previous `jekyll-v4` test fixture to `jekyll-v4-al2` (uses AL2
build image)
* Update `jekyll-v4` test fixture to use Jekyll 4.3.3 (required for Ruby
3.3.0), and remove `package.json` so that it uses AL2023 build image
* Copy previous `middleman-v4` test fixture to `middleman-v4-al2` (uses
AL2 build image)
* Removed `package.json` from `middleman-v4` fixture, so that it uses
AL2023 build image
2024-05-02 00:57:59 -07:00
JJ Kasper
58c6755e0c [next] Fix interception routes PPR route handling (#11527)
This ensures we properly create `.prefetch.rsc` output aliases for non-prerender outputs when PPR is enabled so that we can properly route which handles the interception route case as that is always dynamic so was missing it's `.prefetch.rsc` output causing the prefetch request to 404 unexpectedly. 

x-ref: [slack thread](https://vercel.slack.com/archives/C070ZMMKVU7/p1714511126337289)
2024-05-01 22:40:04 +00:00
dependabot[bot]
f18575fcd0 [framework-fixtures]: Bump preact from 10.20.1 to 10.21.0 in /packages/static-build/test/fixtures/preact-v10 in the core group (#11517)
Bumps the core group in /packages/static-build/test/fixtures/preact-v10 with 1 update: [preact](https://github.com/preactjs/preact).

Updates `preact` from 10.20.1 to 10.21.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/preactjs/preact/releases">preact's releases</a>.</em></p>
<blockquote>
<h2>10.21.0</h2>
<h2>Features</h2>
<ul>
<li>Debug throw on too many rerenders (<a href="https://redirect.github.com/preactjs/preact/issues/4349">#4349</a>, thanks <a href="https://github.com/rschristian"><code>@​rschristian</code></a>)</li>
<li>Add compat/client types (<a href="https://redirect.github.com/preactjs/preact/issues/4345">#4345</a>, thanks <a href="https://github.com/rschristian"><code>@​rschristian</code></a>)</li>
</ul>
<h2>Fixes</h2>
<ul>
<li>Expose hooks through compat's <code>ReactCurrentDispatcher</code> (<a href="https://redirect.github.com/preactjs/preact/issues/4342">#4342</a>, thanks <a href="https://github.com/rschristian"><code>@​rschristian</code></a>)</li>
<li>Respect default value (<a href="https://redirect.github.com/preactjs/preact/issues/4341">#4341</a>, thanks <a href="https://github.com/JoviDeCroock"><code>@​JoviDeCroock</code></a>)</li>
<li>Incorrect &quot;missing transform-jsx-source&quot; warning (<a href="https://redirect.github.com/preactjs/preact/issues/4350">#4350</a>, thanks <a href="https://github.com/rschristian"><code>@​rschristian</code></a>)</li>
</ul>
<h2>Types</h2>
<ul>
<li>Support ComponentChild(ren) in compat render/hydrate/createPortal (<a href="https://redirect.github.com/preactjs/preact/issues/4346">#4346</a>, thanks <a href="https://github.com/rschristian"><code>@​rschristian</code></a>)</li>
<li>Import and re-export PreactElement (<a href="https://redirect.github.com/preactjs/preact/issues/3228">#3228</a>, thanks <a href="https://github.com/henryqdineen"><code>@​henryqdineen</code></a>)</li>
</ul>
<h2>Maintenance</h2>
<ul>
<li>Add zustand and redux-toolkit to the demo. (<a href="https://redirect.github.com/preactjs/preact/issues/3523">#3523</a>, thanks <a href="https://github.com/MortezaMirjavadi"><code>@​MortezaMirjavadi</code></a>)</li>
<li>Optimise jsx runtime (<a href="https://redirect.github.com/preactjs/preact/issues/4337">#4337</a>, thanks <a href="https://github.com/JoviDeCroock"><code>@​JoviDeCroock</code></a>)</li>
</ul>
<h2>10.20.2</h2>
<h2>Fixes</h2>
<ul>
<li>Check whether <code>oldDom</code> is present in the DOM (<a href="https://redirect.github.com/preactjs/preact/issues/4318">#4318</a>, thanks <a href="https://github.com/JoviDeCroock"><code>@​JoviDeCroock</code></a>)</li>
<li>Simplify the logic introduced in <a href="https://redirect.github.com/preactjs/preact/issues/4322">#4322</a> &amp; use eventClock for capture events too (<a href="https://redirect.github.com/preactjs/preact/issues/4324">#4324</a>, thanks <a href="https://github.com/jviide"><code>@​jviide</code></a>)</li>
<li>Use a virtual clock instead of Date.now() for event dispatch times (<a href="https://redirect.github.com/preactjs/preact/issues/4322">#4322</a>, thanks <a href="https://github.com/jviide"><code>@​jviide</code></a>)</li>
</ul>
<h2>Types</h2>
<ul>
<li>Add template tag JSX type (<a href="https://redirect.github.com/preactjs/preact/issues/4334">#4334</a>, thanks <a href="https://github.com/marvinhagemeister"><code>@​marvinhagemeister</code></a>)</li>
</ul>
<h2>Maintenance</h2>
<ul>
<li>Integrate the new benchmarks repo and update  (<a href="https://redirect.github.com/preactjs/preact/issues/4310">#4310</a>, thanks <a href="https://github.com/andrewiggins"><code>@​andrewiggins</code></a>)</li>
<li>Some byte improvements (<a href="https://redirect.github.com/preactjs/preact/issues/4321">#4321</a>, thanks <a href="https://github.com/JoviDeCroock"><code>@​JoviDeCroock</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="a832512a4e"><code>a832512</code></a> 10.21.0 (<a href="https://redirect.github.com/preactjs/preact/issues/4360">#4360</a>)</li>
<li><a href="dbc4d6109c"><code>dbc4d61</code></a> Merge pull request <a href="https://redirect.github.com/preactjs/preact/issues/4350">#4350</a> from preactjs/fix/jsx-source-debug-warning</li>
<li><a href="50c3f3ebe8"><code>50c3f3e</code></a> Merge branch 'main' into fix/jsx-source-debug-warning</li>
<li><a href="613cacc0e3"><code>613cacc</code></a> feat: debug throw on too many rerenders (<a href="https://redirect.github.com/preactjs/preact/issues/4349">#4349</a>)</li>
<li><a href="2e2f5ab3a7"><code>2e2f5ab</code></a> fix: Appease ESLint</li>
<li><a href="d4ff12e97c"><code>d4ff12e</code></a> test: Add test for fixed jsx-source warning</li>
<li><a href="61ce7a4f26"><code>61ce7a4</code></a> refactor: Slight rename</li>
<li><a href="784af469d1"><code>784af46</code></a> fix: Incorrect &quot;missing <code>transform-jsx-source</code>&quot; warning</li>
<li><a href="aa95aa924d"><code>aa95aa9</code></a> Merge pull request <a href="https://redirect.github.com/preactjs/preact/issues/4346">#4346</a> from preactjs/types/fix-hydrate-render-createPortal</li>
<li><a href="1fc799dc6f"><code>1fc799d</code></a> refactor: Support ComponentChild(ren) in compat render/hydrate/createPortal</li>
<li>Additional commits viewable in <a href="https://github.com/preactjs/preact/compare/10.20.1...10.21.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=preact&package-manager=npm_and_yarn&previous-version=10.20.1&new-version=10.21.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>
2024-04-30 21:37:27 +00:00
Erika Rowland
d6412be6bc Migrate build argument parsing (#11492)
Migrate `vc build` to use updated argument parsing helpers.

Adds `try/catch` and `handleError` logic that was previously missing.
2024-04-30 21:23:41 +00:00
JJ Kasper
52e435aa5d Fix missing .rsc outputs for pages prerenders (#11503)
When generating the outputs for pages when app router is also present we weren't creating the placeholder `.rsc` outputs for all prerender outputs so when a `fallback: false` route was configured in the pages router that could fall through to an app route it causes issues with client-navigation behaving differently than a direct visit. 

This ensures we add the placeholder `.rsc` outputs for all prerenders so that `fallback: false` pages don't have this unexpected behavior.
2024-04-30 20:34:41 +00:00
Vercel Release Bot
303256343a Version Packages (#11523)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## vercel@34.1.7

### Patch Changes

- Updated dependencies
\[[`3420ba015`](3420ba0153)]:
    -   @vercel/next@4.2.6

## @vercel/next@4.2.6

### Patch Changes

- [next] Update test fixture version
([#11514](https://github.com/vercel/vercel/pull/11514))

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-30 13:09:58 -07:00
Vercel Release Bot
88f3a816d9 Version Packages (#11520)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## vercel@34.1.6

### Patch Changes

- Updated dependencies
\[[`b1adaf76e`](b1adaf76ec),
[`3fb97d1d2`](3fb97d1d27)]:
    -   @vercel/next@4.2.5
    -   @vercel/static-build@2.5.2

## @vercel/next@4.2.5

### Patch Changes

- Only rewrite next-action requests to `.action` handlers
([#11504](https://github.com/vercel/vercel/pull/11504))

## @vercel/static-build@2.5.2

### Patch Changes

- Revert `SUPPORTED_RUBY_VERSION` change to fix Ruby site generators on
AL2 build image ([#11522](https://github.com/vercel/vercel/pull/11522))

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-30 13:04:00 -07:00
JJ Kasper
3420ba0153 [next] Update test fixture version (#11514)
Noticed in https://github.com/vercel/vercel/pull/11503 that this test
fixture is incorrectly locked to a specific version when we don't want
it to be.
2024-04-30 13:03:36 -07:00
Nathan Rajlich
3fb97d1d27 [static-build] Add changeset for SUPPORTED_RUBY_VERSION revert (#11522)
A changeset should have been included for
https://github.com/vercel/vercel/pull/11518. Adding that here.
2024-04-30 13:01:59 -07:00
Nathan Rajlich
f11c7024c4 [static-build] Fix running of e2e tests (#11518)
We accidentally committed a change which prevented the static-build e2e
tests, so fixing that.

Also reverting a change from https://github.com/vercel/vercel/pull/11497
which was causing issues with Ruby-based static site generators when
using the AL2 build image.
2024-04-30 12:57:25 -07:00
Sean Massa
5eb4cc6f5d Revert "[framework-fixtures]: Bump the core group in /packages/static-build/test/fixtures/hydrogen-v2023 with 6 updates" (#11521)
Reverts vercel/vercel#11480

This was merged in when we weren't running the tests.
([Fix](https://github.com/vercel/vercel/pull/11518)).
2024-04-30 12:35:07 -07:00
Zack Tanner
b1adaf76ec [next] Only rewrite next-action requests to .action handlers (#11504)
This only rewrites requests that contain a `next-action` header (explicitly indicating it's a server action). A side effect is that POST requests to a server action on a static route, without a next-action header, won't be marked as streaming (but will still execute normally). This is fine as only the handled action needs to stream. 

This relands .action handling behind a feature flag.
2024-04-30 18:32:54 +00:00
114 changed files with 8037 additions and 1814 deletions

View File

@@ -37,7 +37,7 @@ You can use the `dev` script to run local changes as if you were invoking Vercel
When contributing to this repository, please first discuss the change you wish to make via [GitHub Discussions](https://github.com/vercel/vercel/discussions/new) with the owners of this repository before submitting a Pull Request.
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and follow it in all your interactions with the project.
Please read our [Code of Conduct](./.github/CODE_OF_CONDUCT.md) and follow it in all your interactions with the project.
### Local development

View File

@@ -1,5 +1,36 @@
# vercel
## 34.1.9
### Patch Changes
- Updated dependencies [[`5a532a5b9`](https://github.com/vercel/vercel/commit/5a532a5b948994ba04783ac560357eed9f94a3f3), [`50fc27ba5`](https://github.com/vercel/vercel/commit/50fc27ba5773870956300bbbaffbe387d549bc12), [`c1d852295`](https://github.com/vercel/vercel/commit/c1d85229509dd319a1f11beb940a759113564d33), [`a5ea04154`](https://github.com/vercel/vercel/commit/a5ea04154ba26ee4e635d8953aa4f0d9d82d3a96)]:
- @vercel/next@4.2.8
- @vercel/node@3.1.0
## 34.1.8
### Patch Changes
- Updated dependencies [[`52e435aa5`](https://github.com/vercel/vercel/commit/52e435aa5d7b4014d19477969ad5cbfbe94aa76f), [`124846a3e`](https://github.com/vercel/vercel/commit/124846a3e65a3bf1ae82327fd4ba2b132674fb39), [`dc974b679`](https://github.com/vercel/vercel/commit/dc974b6797de0b6e90373c92e1f2bbdafcfc6687), [`58c6755e0`](https://github.com/vercel/vercel/commit/58c6755e0c12cae2ce55978b7bf8722133151196)]:
- @vercel/next@4.2.7
- @vercel/static-build@2.5.3
## 34.1.7
### Patch Changes
- Updated dependencies [[`3420ba015`](https://github.com/vercel/vercel/commit/3420ba0153dcabffef7114ba2361fb0f3c43a7b3)]:
- @vercel/next@4.2.6
## 34.1.6
### Patch Changes
- Updated dependencies [[`b1adaf76e`](https://github.com/vercel/vercel/commit/b1adaf76ec17d1bbfe30a2bf65405bd886fa9bcf), [`3fb97d1d2`](https://github.com/vercel/vercel/commit/3fb97d1d270e835ce34a687bd234ea53dfe446a2)]:
- @vercel/next@4.2.5
- @vercel/static-build@2.5.2
## 34.1.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "34.1.5",
"version": "34.1.9",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -36,13 +36,13 @@
"@vercel/fun": "1.1.0",
"@vercel/go": "3.1.1",
"@vercel/hydrogen": "1.0.2",
"@vercel/next": "4.2.4",
"@vercel/node": "3.0.28",
"@vercel/next": "4.2.8",
"@vercel/node": "3.1.0",
"@vercel/python": "4.2.0",
"@vercel/redwood": "2.0.8",
"@vercel/remix-builder": "2.1.5",
"@vercel/ruby": "2.1.0",
"@vercel/static-build": "2.5.1",
"@vercel/static-build": "2.5.3",
"chokidar": "3.3.1"
},
"devDependencies": {

View File

@@ -10,7 +10,7 @@ export const buildCommand: Command = {
name: 'prod',
description: 'Build a production deployment',
shorthand: null,
type: String,
type: Boolean,
deprecated: false,
},
{

View File

@@ -43,7 +43,7 @@ import type { VercelConfig } from '@vercel/client';
import pull from '../pull';
import { staticFiles as getFiles } from '../../util/get-files';
import Client from '../../util/client';
import getArgs from '../../util/get-args';
import { parseArguments } from '../../util/get-args';
import cmd from '../../util/output/cmd';
import * as cli from '../../util/pkg-name';
import cliPkg from '../../util/pkg';
@@ -66,12 +66,13 @@ import {
import { importBuilders } from '../../util/build/import-builders';
import { initCorepack, cleanupCorepack } from '../../util/build/corepack';
import { sortBuilders } from '../../util/build/sort-builders';
import { toEnumerableError } from '../../util/error';
import { handleError, toEnumerableError } from '../../util/error';
import { validateConfig } from '../../util/validate-config';
import { setMonorepoDefaultSettings } from '../../util/build/monorepo';
import { help } from '../help';
import { buildCommand } from './command';
import { scrubArgv } from '../../util/build/scrub-argv';
import { getFlagsSpecification } from '../../util/get-flags-specification';
type BuildResult = BuildResultV2 | BuildResultV3;
@@ -133,22 +134,26 @@ export default async function main(client: Client): Promise<number> {
process.env.__VERCEL_BUILD_RUNNING = '1';
}
// Parse CLI args
const argv = getArgs(client.argv.slice(2), {
'--output': String,
'--prod': Boolean,
'--yes': Boolean,
'-y': '--yes',
});
let parsedArgs = null;
if (argv['--help']) {
const flagsSpecification = getFlagsSpecification(buildCommand.options);
// Parse CLI args
try {
parsedArgs = parseArguments(client.argv.slice(2), flagsSpecification);
} catch (error) {
handleError(error);
return 1;
}
if (parsedArgs.flags['--help']) {
output.print(help(buildCommand, { columns: client.stderr.columns }));
return 2;
}
// Build `target` influences which environment variables will be used
const target = argv['--prod'] ? 'production' : 'preview';
const yes = Boolean(argv['--yes']);
const target = parsedArgs.flags['--prod'] ? 'production' : 'preview';
const yes = Boolean(parsedArgs.flags['--yes']);
try {
await validateNpmrc(cwd);
@@ -213,8 +218,8 @@ export default async function main(client: Client): Promise<number> {
// Delete output directory from potential previous build
const defaultOutputDir = join(cwd, projectRootDirectory, OUTPUT_DIR);
const outputDir = argv['--output']
? resolve(argv['--output'])
const outputDir = parsedArgs.flags['--output']
? resolve(parsedArgs.flags['--output'])
: defaultOutputDir;
await Promise.all([
fs.remove(outputDir),

View File

@@ -0,0 +1,7 @@
# @vercel/functions
## 1.0.0
### Major Changes
- Initial release ([#11553](https://github.com/vercel/vercel/pull/11553))

17
packages/functions/index.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Extends the lifetime of the request handler for the lifetime of the given {@link Promise}
* @see https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil
*
* @param promise The promise to wait for.
* @example
*
* ```
* import { waitUntil } from '@vercel/functions';
*
* export function GET(request) {
* waitUntil(fetch('https://vercel.com'));
* return new Response('OK');
* }
* ```
*/
export const waitUntil: (promise: Promise<unknown>) => void;

View File

@@ -0,0 +1,24 @@
/* global globalThis */
export const waitUntil = promise => {
if (
promise instanceof Promise === false ||
promise === null ||
typeof promise !== 'object' ||
typeof promise.then !== 'function'
) {
throw new TypeError(
`waitUntil can only be called with a Promise, got ${typeof promise}`
);
}
const ctx = globalThis[Symbol.for('@vercel/request-context')]?.get?.() ?? {};
if (!ctx.waitUntil) {
throw new Error(
'failed to get waitUntil function for this request context'
);
}
ctx.waitUntil(promise);
};

View File

@@ -0,0 +1,45 @@
/* global globalThis */
import { vi, expect, test } from 'vitest';
import { waitUntil } from '.';
test.each([
{},
() => {},
function () {},
NaN,
1,
false,
undefined,
null,
[],
'▲',
])('waitUntil throws when called with %s', input => {
expect(() => waitUntil(input)).toThrow(TypeError);
expect(() => waitUntil(input)).toThrow(
`waitUntil can only be called with a Promise, got ${typeof input}`
);
});
test.each([null, undefined, {}])(
'waitUntil throws when context is %s',
input => {
const promise = Promise.resolve();
globalThis[Symbol.for('@vercel/request-context')] = input;
expect(() => waitUntil(promise)).toThrow(Error);
expect(() => waitUntil(promise)).toThrow(
'failed to get waitUntil function for this request context'
);
}
);
test('waitUntil calls ctx.waitUntil when available', async () => {
const promise = Promise.resolve();
const waitUntilMock = vi.fn().mockReturnValue(promise);
globalThis[Symbol.for('@vercel/request-context')] = {
get: () => ({ waitUntil: waitUntilMock }),
};
waitUntil(promise);
expect(waitUntilMock).toHaveBeenCalledWith(promise);
});

View File

@@ -0,0 +1,33 @@
{
"name": "@vercel/functions",
"description": "Runtime functions to be used with your Vercel Functions",
"homepage": "https://vercel.com",
"version": "1.0.0",
"types": "index.d.ts",
"main": "index.js",
"repository": {
"directory": "packages/functions",
"type": "git",
"url": "git+https://github.com/vercel/vercel.git"
},
"bugs": {
"url": "https://github.com/vercel/vercel/issues"
},
"devDependencies": {
"vitest": "1.3.1"
},
"engines": {
"node": ">= 16"
},
"files": [
"index.d.ts",
"index.js"
],
"scripts": {
"test": "vitest"
},
"license": "Apache-2.0",
"publishConfig": {
"access": "public"
}
}

View File

@@ -1,5 +1,37 @@
# @vercel/next
## 4.2.8
### Patch Changes
- Fix missing initial RSC headers ([#11552](https://github.com/vercel/vercel/pull/11552))
- Remove .prefetch.rsc rewrites for non-PPR ([#11540](https://github.com/vercel/vercel/pull/11540))
- [next] rename middleware manifest env ([#11549](https://github.com/vercel/vercel/pull/11549))
## 4.2.7
### Patch Changes
- Fix missing .rsc outputs for pages prerenders ([#11503](https://github.com/vercel/vercel/pull/11503))
- Fix base path app index RSC handling ([#11528](https://github.com/vercel/vercel/pull/11528))
- Fix interception routes PPR route handling ([#11527](https://github.com/vercel/vercel/pull/11527))
## 4.2.6
### Patch Changes
- [next] Update test fixture version ([#11514](https://github.com/vercel/vercel/pull/11514))
## 4.2.5
### Patch Changes
- Only rewrite next-action requests to `.action` handlers ([#11504](https://github.com/vercel/vercel/pull/11504))
## 4.2.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "4.2.4",
"version": "4.2.8",
"license": "Apache-2.0",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",

View File

@@ -1928,6 +1928,7 @@ export const build: BuildV2 = async ({
bypassToken: prerenderManifest.bypassToken || '',
isServerMode,
experimentalPPRRoutes,
hasActionOutputSupport: false,
}).then(arr =>
localizeDynamicRoutes(
arr,
@@ -1958,6 +1959,7 @@ export const build: BuildV2 = async ({
bypassToken: prerenderManifest.bypassToken || '',
isServerMode,
experimentalPPRRoutes,
hasActionOutputSupport: false,
}).then(arr =>
arr.map(route => {
route.src = route.src.replace('^', `^${dynamicPrefix}`);

View File

@@ -51,6 +51,7 @@ import {
normalizePrefetches,
CreateLambdaFromPseudoLayersOptions,
getPostponeResumePathname,
LambdaGroup,
MAX_UNCOMPRESSED_LAMBDA_SIZE,
} from './utils';
import {
@@ -68,6 +69,7 @@ const CORRECT_NOT_FOUND_ROUTES_VERSION = 'v12.0.1';
const CORRECT_MIDDLEWARE_ORDER_VERSION = 'v12.1.7-canary.29';
const NEXT_DATA_MIDDLEWARE_RESOLVING_VERSION = 'v12.1.7-canary.33';
const EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION = 'v12.2.0';
const ACTION_OUTPUT_SUPPORT_VERSION = 'v14.2.2';
const CORRECTED_MANIFESTS_VERSION = 'v12.2.0';
// Ideally this should be in a Next.js manifest so we can change it in
@@ -199,6 +201,9 @@ export async function serverBuild({
nextVersion,
EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION
);
const hasActionOutputSupport =
semver.gte(nextVersion, ACTION_OUTPUT_SUPPORT_VERSION) &&
Boolean(process.env.NEXT_EXPERIMENTAL_STREAMING_ACTIONS);
const projectDir = requiredServerFilesManifest.relativeAppDir
? path.join(baseDir, requiredServerFilesManifest.relativeAppDir)
: requiredServerFilesManifest.appDir || entryPath;
@@ -246,7 +251,7 @@ export async function serverBuild({
if (rewrite.src && rewrite.dest) {
rewrite.src = rewrite.src.replace(
/\/?\(\?:\/\)\?/,
'(?<rscsuff>(\\.prefetch)?\\.rsc)?(?:/)?'
`(?<rscsuff>${experimental.ppr ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?`
);
let destQueryIndex = rewrite.dest.indexOf('?');
@@ -926,11 +931,23 @@ export async function serverBuild({
inversedAppPathManifest,
});
const appRouterStreamingActionLambdaGroups: LambdaGroup[] = [];
for (const group of appRouterLambdaGroups) {
if (!group.isPrerenders || group.isExperimentalPPR) {
group.isStreaming = true;
}
group.isAppRouter = true;
// We create a streaming variant of the Prerender lambda group
// to support actions that are part of a Prerender
if (hasActionOutputSupport) {
appRouterStreamingActionLambdaGroups.push({
...group,
isActionLambda: true,
isStreaming: true,
});
}
}
for (const group of appRouteHandlersLambdaGroups) {
@@ -982,6 +999,13 @@ export async function serverBuild({
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
appRouterStreamingPrerenderLambdaGroups:
appRouterStreamingActionLambdaGroups.map(group => ({
pages: group.pages,
isPrerender: group.isPrerenders,
pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})),
appRouteHandlersLambdaGroups: appRouteHandlersLambdaGroups.map(
group => ({
pages: group.pages,
@@ -999,6 +1023,7 @@ export async function serverBuild({
const combinedGroups = [
...pageLambdaGroups,
...appRouterLambdaGroups,
...appRouterStreamingActionLambdaGroups,
...apiLambdaGroups,
...appRouteHandlersLambdaGroups,
];
@@ -1208,6 +1233,11 @@ export async function serverBuild({
let outputName = path.posix.join(entryDirectory, pageName);
if (group.isActionLambda) {
// give the streaming prerenders a .action suffix
outputName = `${outputName}.action`;
}
// If this is a PPR page, then we should prefix the output name.
if (isPPR) {
if (!revalidate) {
@@ -1378,6 +1408,7 @@ export async function serverBuild({
isServerMode: true,
dynamicMiddlewareRouteMap: middleware.dynamicRouteMap,
experimentalPPRRoutes,
hasActionOutputSupport,
}).then(arr =>
localizeDynamicRoutes(
arr,
@@ -1557,10 +1588,19 @@ export async function serverBuild({
if (lambdas[pathname]) {
lambdas[`${pathname}.rsc`] = lambdas[pathname];
if (experimental.ppr) {
lambdas[`${pathname}${RSC_PREFETCH_SUFFIX}`] = lambdas[pathname];
}
}
if (edgeFunctions[pathname]) {
edgeFunctions[`${pathname}.rsc`] = edgeFunctions[pathname];
if (experimental.ppr) {
edgeFunctions[`${pathname}${RSC_PREFETCH_SUFFIX}`] =
edgeFunctions[pathname];
}
}
}
}
@@ -1863,7 +1903,7 @@ export async function serverBuild({
...(appDir
? [
...(rscPrefetchHeader
...(rscPrefetchHeader && experimental.ppr
? [
{
src: `^${path.posix.join('/', entryDirectory, '/')}`,
@@ -1905,9 +1945,44 @@ export async function serverBuild({
},
]
: []),
...(hasActionOutputSupport
? [
// Create rewrites for streaming prerenders (.action routes)
// This contains separate rewrites for each possible "has" (action header, or content-type)
// Also includes separate handling for index routes which should match to /index.action.
// This follows the same pattern as the rewrites for .rsc files.
{
src: `^${path.posix.join('/', entryDirectory, '/')}`,
dest: path.posix.join('/', entryDirectory, '/index.action'),
has: [
{
type: 'header',
key: 'next-action',
},
],
continue: true,
override: true,
},
{
src: `^${path.posix.join(
'/',
entryDirectory,
'/((?!.+\\.action).+?)(?:/)?$'
)}`,
dest: path.posix.join('/', entryDirectory, '/$1.action'),
has: [
{
type: 'header',
key: 'next-action',
},
],
continue: true,
override: true,
},
]
: []),
{
src: `^${path.posix.join('/', entryDirectory, '/')}`,
src: `^${path.posix.join('/', entryDirectory, '/?')}`,
has: [
{
type: 'header',
@@ -1970,47 +2045,6 @@ export async function serverBuild({
]
: []),
...(rscPrefetchHeader && !experimental.ppr
? [
{
src: path.posix.join(
'/',
entryDirectory,
`/__index${RSC_PREFETCH_SUFFIX}`
),
dest: path.posix.join('/', entryDirectory, '/index.rsc'),
has: [
{
type: 'header',
key: rscPrefetchHeader,
},
],
continue: true,
override: true,
},
{
src: `^${path.posix.join(
'/',
entryDirectory,
`/(.+?)${RSC_PREFETCH_SUFFIX}(?:/)?$`
)}`,
dest: path.posix.join(
'/',
entryDirectory,
`/$1${experimental.ppr ? RSC_PREFETCH_SUFFIX : '.rsc'}`
),
has: [
{
type: 'header',
key: rscPrefetchHeader,
},
],
continue: true,
override: true,
},
]
: []),
// These need to come before handle: miss or else they are grouped
// with that routing section
...afterFilesRewrites,

View File

@@ -321,6 +321,7 @@ export async function getDynamicRoutes({
isServerMode,
dynamicMiddlewareRouteMap,
experimentalPPRRoutes,
hasActionOutputSupport,
}: {
entryPath: string;
entryDirectory: string;
@@ -333,6 +334,7 @@ export async function getDynamicRoutes({
isServerMode?: boolean;
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>;
experimentalPPRRoutes: ReadonlySet<string>;
hasActionOutputSupport: boolean;
}): Promise<RouteWithSrc[]> {
if (routesManifest) {
switch (routesManifest.version) {
@@ -423,14 +425,25 @@ export async function getDynamicRoutes({
});
}
routes.push({
...route,
src: route.src.replace(
new RegExp(escapeStringRegexp('(?:/)?$')),
'(?:\\.rsc)(?:/)?$'
),
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
});
if (hasActionOutputSupport) {
routes.push({
...route,
src: route.src.replace(
new RegExp(escapeStringRegexp('(?:/)?$')),
'(?<nxtsuffix>(?:\\.action|\\.rsc))(?:/)?$'
),
dest: route.dest?.replace(/($|\?)/, '$nxtsuffix$1'),
});
} else {
routes.push({
...route,
src: route.src.replace(
new RegExp(escapeStringRegexp('(?:/)?$')),
'(?:\\.rsc)(?:/)?$'
),
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
});
}
routes.push(route);
}
@@ -1487,6 +1500,7 @@ export type LambdaGroup = {
isStreaming?: boolean;
isPrerenders?: boolean;
isExperimentalPPR?: boolean;
isActionLambda?: boolean;
isPages?: boolean;
isApiLambda: boolean;
pseudoLayer: PseudoLayer;
@@ -2451,6 +2465,7 @@ export const onPrerenderRoute =
...(rscEnabled
? {
initialHeaders: {
...initialHeaders,
'content-type': rscContentTypeHeader,
vary: rscVaryHeader,
// If it contains a pre-render, then it was postponed.
@@ -2463,6 +2478,22 @@ export const onPrerenderRoute =
});
}
// we need to ensure all prerenders have a matching .rsc output
// otherwise routing could fall through unexpectedly for the
// fallback: false case as it doesn't have a dynamic route
// to catch the `.rsc` request for app -> pages routing
if (outputPrerenderPathData?.endsWith('.json') && appDir) {
const dummyOutput = new FileBlob({
data: '{}',
contentType: 'application/json',
});
const rscKey = `${outputPathPage}.rsc`;
const prefetchRscKey = `${outputPathPage}${RSC_PREFETCH_SUFFIX}`;
prerenders[rscKey] = dummyOutput;
prerenders[prefetchRscKey] = dummyOutput;
}
++prerenderGroup;
if (routesManifest?.i18n && isBlocking) {
@@ -2754,7 +2785,7 @@ interface EdgeFunctionInfoV2 extends BaseEdgeFunctionInfo {
interface EdgeFunctionInfoV3 extends BaseEdgeFunctionInfo {
matchers: EdgeFunctionMatcher[];
environments: Record<string, string>;
env: Record<string, string>;
}
interface EdgeFunctionMatcher {
@@ -2989,7 +3020,7 @@ export async function getMiddlewareBundle({
slug: 'nextjs',
version: nextVersion,
},
environment: edgeFunction.environments,
environment: edgeFunction.env,
});
})(),
routeMatchers: getRouteMatchers(edgeFunction, routesManifest),
@@ -3169,7 +3200,7 @@ export function upgradeMiddlewareManifestV1(
return {
...rest,
matchers: [{ regexp }],
environments: {},
env: {},
};
}
@@ -3197,7 +3228,7 @@ export function upgradeMiddlewareManifestV2(
const { ...rest } = v2Info;
return {
...rest,
environments: {},
env: {},
};
}

View File

@@ -0,0 +1,6 @@
'use server';
export async function increment(value) {
await new Promise(resolve => setTimeout(resolve, 500));
return value + 1;
}

View File

@@ -0,0 +1,19 @@
'use client';
import { useState } from 'react';
import { increment } from '../../../actions';
export default function Page() {
const [count, setCount] = useState(0);
async function updateCount() {
const newCount = await increment(count);
setCount(newCount);
}
return (
<form action={updateCount}>
<div id="count">{count}</div>
<button>Submit</button>
</form>
);
}

View File

@@ -0,0 +1,5 @@
export const dynamic = 'force-dynamic';
export default function Layout({ children }) {
return children;
}

View File

@@ -0,0 +1,19 @@
'use client';
import { useState } from 'react';
import { increment } from '../../../actions';
export default function Page() {
const [count, setCount] = useState(0);
async function updateCount() {
const newCount = await increment(count);
setCount(newCount);
}
return (
<form action={updateCount}>
<div id="count">{count}</div>
<button>Submit</button>
</form>
);
}

View File

@@ -0,0 +1,5 @@
export const dynamic = 'force-static';
export default function Layout({ children }) {
return children;
}

View File

@@ -0,0 +1,19 @@
'use client';
import { useState } from 'react';
import { increment } from '../../actions';
export default function Page() {
const [count, setCount] = useState(0);
async function updateCount() {
const newCount = await increment(count);
setCount(newCount);
}
return (
<form action={updateCount}>
<div id="count">{count}</div>
<button>Submit</button>
</form>
);
}

View File

@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html>
<head>
<title>Hello World</title>
</head>
<body>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
// @ts-ignore
import { useCallback, useState } from 'react';
function request(method) {
return fetch('/api/test', {
method,
headers: {
'content-type': 'multipart/form-data;.*',
},
});
}
export default function Home() {
const [result, setResult] = useState('Press submit');
const onClick = useCallback(async method => {
const res = await request(method);
const text = await res.text();
setResult(res.ok ? `${method} ${text}` : 'Error: ' + res.status);
}, []);
return (
<>
<div className="flex flex-col items-center justify-center h-screen">
<div className="flex flex-row space-x-2 items-center justify-center">
<button
className="border border-white rounded-sm p-4 mb-4"
onClick={() => onClick('GET')}
>
Submit GET
</button>
<button
className="border border-white rounded-sm p-4 mb-4"
onClick={() => onClick('POST')}
>
Submit POST
</button>
</div>
<div className="text-white">{result}</div>
</div>
</>
);
}

View File

@@ -0,0 +1,5 @@
export const dynamic = 'force-dynamic';
export default function Layout({ children }) {
return children;
}

View File

@@ -0,0 +1,15 @@
import { revalidatePath } from 'next/cache';
export default async function Page() {
async function serverAction() {
'use server';
await new Promise(resolve => setTimeout(resolve, 1000));
revalidatePath('/dynamic');
}
return (
<form action={serverAction}>
<button>Submit</button>
</form>
);
}

View File

@@ -0,0 +1,15 @@
import { revalidatePath } from 'next/cache';
export default async function Page() {
async function serverAction() {
'use server';
await new Promise(resolve => setTimeout(resolve, 1000));
revalidatePath('/dynamic');
}
return (
<form action={serverAction}>
<button>Submit</button>
</form>
);
}

View File

@@ -0,0 +1,19 @@
import { revalidatePath } from 'next/cache';
export default async function Page() {
async function serverAction() {
'use server';
await new Promise(resolve => setTimeout(resolve, 1000));
revalidatePath('/dynamic');
}
return (
<form action={serverAction}>
<button>Submit</button>
</form>
);
}
export function generateStaticParams() {
return [{ slug: 'pre-generated' }];
}

View File

@@ -0,0 +1,5 @@
export const dynamic = 'force-static';
export default function Layout({ children }) {
return children;
}

View File

@@ -0,0 +1,15 @@
import { revalidatePath } from 'next/cache'
export default async function Page() {
async function serverAction() {
'use server';
await new Promise((resolve) => setTimeout(resolve, 1000));
revalidatePath('/dynamic');
}
return (
<form action={serverAction}>
<button>Submit</button>
</form>
);
}

View File

@@ -0,0 +1,236 @@
/* eslint-env jest */
const path = require('path');
const { deployAndTest } = require('../../utils');
const fetch = require('../../../../../test/lib/deployment/fetch-retry');
const ctx = {};
function findActionId(page) {
page = `app${page}/page`; // add /app prefix and /page suffix
for (const [actionId, details] of Object.entries(ctx.actionManifest.node)) {
if (details.workers[page]) {
return actionId;
}
}
return null;
}
function generateFormDataPayload(actionId) {
return {
method: 'POST',
body: `------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"1_$ACTION_ID_${actionId}\"\r\n\r\n\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n[\"$K1\"]\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ--\r\n`,
headers: {
'Content-Type':
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
'Next-Action': actionId,
},
};
}
describe(`${__dirname.split(path.sep).pop()}`, () => {
beforeAll(async () => {
const info = await deployAndTest(__dirname);
const actionManifest = await fetch(
`${info.deploymentUrl}/server-reference-manifest.json`
).then(res => res.json());
ctx.actionManifest = actionManifest;
Object.assign(ctx, info);
});
describe('client component', () => {
it('should bypass the static cache for a server action', async () => {
const path = '/client/static';
const actionId = findActionId(path);
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
method: 'POST',
body: JSON.stringify([1337]),
headers: {
'Content-Type': 'text/plain;charset=UTF-8',
'Next-Action': actionId,
},
});
expect(res.status).toEqual(200);
const body = await res.text();
expect(body).toContain('1338');
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
it('should bypass the static cache for a server action on a page with dynamic params', async () => {
const path = '/client/static/[dynamic-static]';
const actionId = findActionId(path);
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
method: 'POST',
body: JSON.stringify([1337]),
headers: {
'Content-Type': 'text/plain;charset=UTF-8',
'Next-Action': actionId,
},
});
expect(res.status).toEqual(200);
const body = await res.text();
expect(body).toContain('1338');
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
it('should bypass the static cache for a multipart request (no action header)', async () => {
const path = '/client/static';
const actionId = findActionId(path);
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
method: 'POST',
body: `------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"1_$ACTION_ID_${actionId}\"\r\n\r\n\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n[\"$K1\"]\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ--\r\n`,
headers: {
'Content-Type':
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
},
});
expect(res.status).toEqual(200);
expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8');
// This is a "BYPASS" because no action ID was provided, so it'll fall back to
// `experimentalBypassFor` handling.
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-matched-path')).toBe(path);
});
it('should properly invoke the action on a dynamic page', async () => {
const path = '/client/dynamic/[id]';
const actionId = findActionId(path);
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
method: 'POST',
body: JSON.stringify([1337]),
headers: {
'Content-Type': 'text/plain;charset=UTF-8',
'Next-Action': actionId,
},
});
expect(res.status).toEqual(200);
const body = await res.text();
expect(body).toContain('1338');
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
});
describe('server component', () => {
it('should bypass the static cache for a server action', async () => {
const path = '/rsc/static';
const actionId = findActionId(path);
const res = await fetch(
`${ctx.deploymentUrl}${path}`,
generateFormDataPayload(actionId)
);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
it('should bypass the static cache for a server action on a page with dynamic params', async () => {
const path = '/rsc/static/[dynamic-static]';
const actionId = findActionId(path);
const res = await fetch(
`${ctx.deploymentUrl}${path}`,
generateFormDataPayload(actionId)
);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
it('should properly invoke the action on a dynamic page', async () => {
const path = '/rsc/dynamic';
const actionId = findActionId(path);
const res = await fetch(
`${ctx.deploymentUrl}${path}`,
generateFormDataPayload(actionId)
);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
describe('generateStaticParams', () => {
it('should bypass the static cache for a server action when pre-generated', async () => {
const path = '/rsc/static/generate-static-params/pre-generated';
const actionId = findActionId(
'/rsc/static/generate-static-params/[slug]'
);
const res = await fetch(
`${ctx.deploymentUrl}${path}`,
generateFormDataPayload(actionId)
);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(
'/rsc/static/generate-static-params/[slug].action'
);
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
it('should bypass the static cache for a server action when not pre-generated', async () => {
const page = '/rsc/static/generate-static-params/[slug]';
const actionId = findActionId(page);
const res = await fetch(
`${ctx.deploymentUrl}/rsc/static/generate-static-params/not-pre-generated`,
generateFormDataPayload(actionId)
);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(page + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
});
});
describe('pages', () => {
it('should not attempt to rewrite the action path for a server action (POST)', async () => {
const res = await fetch(`${ctx.deploymentUrl}/api/test`, {
method: 'POST',
headers: {
'Content-Type':
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
},
});
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe('/api/test');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
const body = await res.json();
expect(body).toEqual({ message: 'Hello from Next.js!' });
});
it('should not attempt to rewrite the action path for a server action (GET)', async () => {
const res = await fetch(`${ctx.deploymentUrl}/api/test`);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe('/api/test');
const body = await res.json();
expect(body).toEqual({ message: 'Hello from Next.js!' });
});
});
});

View File

@@ -0,0 +1 @@
module.exports = {};

View File

@@ -0,0 +1,9 @@
{
"dependencies": {
"next": "canary"
},
"scripts": {
"build": "next build && cp .next/server/server-reference-manifest.json public/"
},
"ignoreNextjsUpdates": true
}

View File

@@ -0,0 +1,3 @@
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from Next.js!' });
}

View File

@@ -0,0 +1,14 @@
{
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"build": {
"env": {
"NEXT_EXPERIMENTAL_STREAMING_ACTIONS": "1"
}
},
"probes": []
}

View File

@@ -7,4 +7,4 @@ export default function Root({ children }) {
<body>{children}</body>
</html>
);
}
}

View File

@@ -0,0 +1,45 @@
'use client';
// @ts-ignore
import { useCallback, useState } from 'react';
function request(method) {
return fetch('/api/test', {
method,
headers: {
'content-type': 'multipart/form-data;.*',
},
});
}
export default function Home() {
const [result, setResult] = useState('Press submit');
const onClick = useCallback(async method => {
const res = await request(method);
const text = await res.text();
setResult(res.ok ? `${method} ${text}` : 'Error: ' + res.status);
}, []);
return (
<>
<div className="flex flex-col items-center justify-center h-screen">
<div className="flex flex-row space-x-2 items-center justify-center">
<button
className="border border-white rounded-sm p-4 mb-4"
onClick={() => onClick('GET')}
>
Submit GET
</button>
<button
className="border border-white rounded-sm p-4 mb-4"
onClick={() => onClick('POST')}
>
Submit POST
</button>
</div>
<div className="text-white">{result}</div>
</div>
</>
);
}

View File

@@ -37,6 +37,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
).then(res => res.json());
ctx.actionManifest = actionManifest;
Object.assign(ctx, info);
});
@@ -81,6 +82,25 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
});
it('should bypass the static cache for a multipart request (no action header)', async () => {
const path = '/client/static';
const actionId = findActionId(path);
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
method: 'POST',
body: `------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"1_$ACTION_ID_${actionId}\"\r\n\r\n\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n[\"$K1\"]\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ--\r\n`,
headers: {
'Content-Type':
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
},
});
expect(res.status).toEqual(200);
expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8');
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
expect(res.headers.get('x-matched-path')).toBe(path);
});
it('should properly invoke the action on a dynamic page', async () => {
const path = '/client/dynamic/[id]';
const actionId = findActionId(path);
@@ -98,6 +118,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
const body = await res.text();
expect(body).toContain('1338');
expect(res.headers.get('x-matched-path')).toBe(path);
// This isn't a "BYPASS" because the action wasn't part of a static prerender
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
});
@@ -145,6 +166,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('content-type')).toBe('text/x-component');
// This isn't a "BYPASS" because the action wasn't part of a static prerender
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
@@ -161,7 +183,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(path);
expect(res.headers.get('x-matched-path')).toBe(
'/rsc/static/generate-static-params/pre-generated'
);
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
});
@@ -178,8 +202,36 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(page);
expect(res.headers.get('content-type')).toBe('text/x-component');
// This isn't a "BYPASS" because the action wasn't part of a static prerender
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
});
});
});
describe('pages', () => {
it('should not attempt to rewrite the action path for a server action (POST)', async () => {
const res = await fetch(`${ctx.deploymentUrl}/api/test`, {
method: 'POST',
headers: {
'Content-Type':
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
},
});
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe('/api/test');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
const body = await res.json();
expect(body).toEqual({ message: 'Hello from Next.js!' });
});
it('should not attempt to rewrite the action path for a server action (GET)', async () => {
const res = await fetch(`${ctx.deploymentUrl}/api/test`);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe('/api/test');
const body = await res.json();
expect(body).toEqual({ message: 'Hello from Next.js!' });
});
});
});

View File

@@ -0,0 +1 @@
module.exports = {};

View File

@@ -0,0 +1,3 @@
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from Next.js!' });
}

View File

@@ -18,6 +18,31 @@
}
],
"probes": [
{
"path": "/hello/world",
"status": 200,
"mustContain": "index app page"
},
{
"path": "/hello/world",
"status": 200,
"mustContain": "index app page",
"mustNotContain": "<html",
"headers": {
"RSC": 1
}
},
{
"path": "/hello/world",
"status": 200,
"mustContain": ":",
"mustNotContain": "<html",
"headers": {
"RSC": 1,
"Next-Router-Prefetch": 1
}
},
{
"path": "/hello/world/dashboard/hello",
"status": 200,
@@ -42,14 +67,14 @@
"status": 200,
"mustContain": "hello from /ssg",
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{
"path": "/hello/world/ssg",
"status": 200,
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
@@ -82,14 +107,14 @@
"status": 200,
"mustContain": "hello from app/dashboard/deployments/[id]/settings",
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{
"path": "/hello/world/dashboard/deployments/123/settings",
"status": 200,
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
@@ -102,14 +127,14 @@
"status": 200,
"mustContain": "catchall",
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{
"path": "/hello/world/dashboard/deployments/catchall/something",
"status": 200,
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
@@ -122,7 +147,7 @@
"status": 200,
"mustContain": "hello from app/dashboard",
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{
@@ -142,7 +167,7 @@
},
"responseHeaders": {
"content-type": "text/x-component",
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{

View File

@@ -0,0 +1,5 @@
export default function Page() {
return (
<p>nested app router catch-all</p>
)
}

View File

@@ -1,5 +1,3 @@
export const dynamic = 'force-dynamic'
export default function Page() {
return <p>index app page {Date.now()}</p>;
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<>
product page
</>
)
}

View File

@@ -13,6 +13,10 @@ module.exports = {
source: '/rewritten-to-index',
destination: '/?fromRewrite=1',
},
{
source: '/to-product/:productId.html',
destination: '/products/:productId',
},
];
},
};

View File

@@ -1,6 +1,6 @@
{
"dependencies": {
"next": "13.5.6",
"next": "canary",
"react": "experimental",
"react-dom": "experimental"
},

View File

@@ -0,0 +1,24 @@
export default function Page(props) {
return (
<>
<p>hello from /nested/blog-fallback-false/[slug]</p>
</>
);
}
export function getStaticProps() {
return {
props: {
now: Date.now()
}
}
}
export function getStaticPaths() {
return {
paths: [
{ params: { slug: 'first' } },
],
fallback: false
}
}

View File

@@ -18,6 +18,74 @@
}
],
"probes": [
{
"path": "/to-product/product-id.html",
"status": 200,
"mustContain": "<html"
},
{
"path": "/to-product/product-id.html",
"status": 200,
"mustNotContain": "<html",
"mustContain": ":",
"headers": {
"RSC": 1
}
},
{
"path": "/to-product/product-id.html",
"status": 200,
"mustNotContain": ".prefetch",
"mustContain": ":",
"headers": {
"RSC": 1,
"Next-Router-Prefetch": 1
}
},
{
"path": "/nested/blog-fallback-false/first",
"status": 200,
"mustContain": "hello from /nested/blog-fallback-false"
},
{
"path": "/nested/blog-fallback-false/first",
"status": 200,
"mustContain": "{}",
"mustNotContain": ":",
"responseHeaders": {
"x-matched-path": "/nested/blog-fallback-false/first.rsc"
},
"headers": {
"RSC": 1,
"Next-Router-Prefetch": 1
}
},
{
"path": "/nested/blog-fallback-false/first",
"status": 200,
"mustContain": "{}",
"mustNotContain": ":",
"responseHeaders": {
"x-matched-path": "/nested/blog-fallback-false/first.rsc"
},
"headers": {
"RSC": 1
}
},
{
"path": "/nested/blog-fallback-false/non-existent",
"status": 200,
"mustContain": "nested app router catch-all"
},
{
"path": "/nested/blog-fallback-false/non-existent",
"status": 200,
"mustContain": ":",
"headers": {
"RSC": 1,
"Next-Router-Prefetch": 1
}
},
{
"path": "/dynamic-index/hello/index",
"status": 200,
@@ -156,7 +224,7 @@
"status": 200,
"mustContain": "hello from /ssg",
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{
@@ -185,7 +253,7 @@
"path": "/ssg",
"status": 200,
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
@@ -197,7 +265,7 @@
"path": "/ssg",
"status": 200,
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url",
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch",
"content-type": "text/x-component"
},
"headers": {
@@ -232,14 +300,14 @@
"status": 200,
"mustContain": "hello from app/dashboard/deployments/[id]/settings",
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{
"path": "/dashboard/deployments/123/settings",
"status": 200,
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
@@ -252,14 +320,14 @@
"status": 200,
"mustContain": "catchall",
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{
"path": "/dashboard/deployments/catchall/something",
"status": 200,
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
@@ -272,7 +340,7 @@
"status": 200,
"mustContain": "hello from app/dashboard",
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{
@@ -292,7 +360,7 @@
},
"responseHeaders": {
"content-type": "text/x-component",
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
}
},
{

View File

@@ -0,0 +1,10 @@
import Link from 'next/link'
export default function Modal() {
return (
<>
<p>cart modal!!</p>
<Link href='/'>to index</Link>
</>
)
}

View File

@@ -0,0 +1,3 @@
export default function Modal() {
return null
}

View File

@@ -0,0 +1,10 @@
import Link from 'next/link'
export default function Cart() {
return (
<>
<p>normal cart page</p>
<Link href='/'>to index</Link>
</>
)
}

View File

@@ -0,0 +1,8 @@
export default function Layout({ modal, children }) {
return (
<>
{modal}
{children}
</>
)
}

View File

@@ -1,10 +1,14 @@
import Link from 'next/link'
import React, { Suspense } from 'react'
import { Dynamic } from '../components/dynamic'
export default () => {
return (
<Suspense fallback={<Dynamic pathname="/" fallback />}>
<Dynamic pathname="/" />
</Suspense>
<>
<Suspense fallback={<Dynamic pathname="/" fallback />}>
<Dynamic pathname="/" />
</Suspense>
<Link href='/cart'>to /cart</Link>
</>
)
}

View File

@@ -19,14 +19,6 @@ const links = [
{ href: '/no-suspense/nested/c', tag: 'no suspense, on-demand' },
{ href: '/dynamic/force-dynamic', tag: "dynamic = 'force-dynamic'" },
{ href: '/dynamic/force-static', tag: "dynamic = 'force-static'" },
{ href: '/edge/suspense', tag: 'edge, pre-generated' },
{ href: '/edge/suspense/a', tag: 'edge, pre-generated' },
{ href: '/edge/suspense/b', tag: 'edge, on-demand' },
{ href: '/edge/suspense/c', tag: 'edge, on-demand' },
{ href: '/edge/no-suspense', tag: 'edge, no suspense, pre-generated' },
{ href: '/edge/no-suspense/a', tag: 'edge, no suspense, pre-generated' },
{ href: '/edge/no-suspense/b', tag: 'edge, no suspense, on-demand' },
{ href: '/edge/no-suspense/c', tag: 'edge, no suspense, on-demand' },
];
export const Links = () => {

View File

@@ -44,6 +44,45 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
Object.assign(ctx, info);
});
it('should handle interception route properly', async () => {
const res = await fetch(`${ctx.deploymentUrl}/cart`);
expect(res.status).toBe(200);
expect(await res.text()).toContain('normal cart page');
const res2 = await fetch(`${ctx.deploymentUrl}/cart`, {
headers: {
RSC: '1',
},
});
const res2Body = await res2.text();
expect(res2.status).toBe(200);
expect(res2Body).toContain(':');
expect(res2Body).not.toContain('<html');
const res3 = await fetch(`${ctx.deploymentUrl}/cart`, {
headers: {
RSC: '1',
'Next-Url': '/cart',
'Next-Router-Prefetch': '1',
},
});
const res3Body = await res3.text();
expect(res3.status).toBe(200);
expect(res3Body).toContain(':');
expect(res3Body).not.toContain('<html');
const res4 = await fetch(`${ctx.deploymentUrl}/cart`, {
headers: {
RSC: '1',
'Next-Url': '/cart',
},
});
const res4Body = await res4.text();
expect(res4.status).toBe(200);
expect(res4Body).toContain(':');
expect(res4Body).not.toContain('<html');
});
describe('dynamic pages should resume', () => {
it.each(pages.filter(p => p.dynamic === true))(
'should resume $pathname',

View File

@@ -76,9 +76,9 @@ if (parseInt(process.versions.node.split('.')[0], 10) >= 16) {
// RSC, root-level page.js
expect(buildResult.output['index']).toBeDefined();
expect(buildResult.output['index'].type).toBe('Lambda');
expect(buildResult.output['index'].memory).toBe(512);
expect(buildResult.output['index'].maxDuration).toBe(5);
expect(buildResult.output['index'].type).toBe('Prerender');
expect(buildResult.output['index'].lambda.memory).toBe(512);
expect(buildResult.output['index'].lambda.maxDuration).toBe(5);
expect(buildResult.output['dashboard']).toBeDefined();
expect(buildResult.output['dashboard/another']).toBeDefined();

View File

@@ -1,5 +1,11 @@
# @vercel/node
## 3.1.0
### Minor Changes
- Make waitUntil consistent for Node.js & Edge ([#11553](https://github.com/vercel/vercel/pull/11553))
## 3.0.28
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/node",
"version": "3.0.28",
"version": "3.1.0",
"license": "Apache-2.0",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -23,7 +23,7 @@
"@edge-runtime/node-utils": "2.3.0",
"@edge-runtime/primitives": "4.1.0",
"@edge-runtime/vm": "3.2.0",
"@types/node": "14.18.33",
"@types/node": "16.18.11",
"@vercel/build-utils": "8.0.0",
"@vercel/error-utils": "2.0.2",
"@vercel/nft": "0.26.4",
@@ -50,6 +50,7 @@
"@types/cookie": "0.3.3",
"@types/etag": "1.8.0",
"@types/jest": "29.5.0",
"@vercel/functions": "workspace:*",
"content-type": "1.0.5",
"cookie": "0.4.0",
"cross-env": "7.0.3",

View File

@@ -0,0 +1,30 @@
/**
* The Awaiter class is used to manage and await multiple promises.
*/
export class Awaiter {
private promises: Set<Promise<unknown>> = new Set();
private onError: ((error: Error) => void) | undefined;
constructor({ onError }: { onError?: (error: Error) => void } = {}) {
this.onError = onError ?? console.error;
}
public awaiting = () =>
this.waitForBatch().then(() =>
this.promises.size > 0 ? this.waitForBatch() : Promise.resolve()
);
public waitUntil = (promise: Promise<unknown>) => {
this.promises.add(promise);
};
private waitForBatch = async () => {
const promises = Array.from(this.promises);
this.promises.clear();
await Promise.all(
promises.map(promise =>
Promise.resolve(promise).then(() => undefined, this.onError)
)
);
};
}

View File

@@ -167,7 +167,6 @@ process.on('message', async m => {
if (onExit) {
await onExit();
}
process.exit(0);
default:
console.error(`unknown IPC message from parent:`, m);

View File

@@ -88,7 +88,12 @@ function registerFetchListener(module, options, dependencies) {
`No default or HTTP-named export was found at ${url}. Add one to handle requests. Learn more: https://vercel.link/creating-edge-middleware`
);
}
const response = await respond(handler, event, options, dependencies);
const response = await respond(
(req, ctx) => handler(req, { waitUntil: ctx.waitUntil.bind(ctx) }),
event,
options,
dependencies
);
event.respondWith(response);
} catch (error) {
event.respondWith(toResponseError(error, dependencies.Response));

View File

@@ -7,13 +7,20 @@ import { EdgeRuntime, runServer } from 'edge-runtime';
import { type Dispatcher, Headers, request as undiciRequest } from 'undici';
import { isError } from '@vercel/error-utils';
import { readFileSync } from 'fs';
import { serializeBody, entrypointToOutputPath, logError } from '../utils.js';
import {
serializeBody,
entrypointToOutputPath,
logError,
waitUntilWarning,
WAIT_UNTIL_TIMEOUT_MS,
} from '../utils.js';
import esbuild from 'esbuild';
import { buildToHeaders } from '@edge-runtime/node-utils';
import type { VercelProxyResponse } from '../types.js';
import type { IncomingMessage } from 'http';
import { fileURLToPath } from 'url';
import { EdgeRuntimeServer } from 'edge-runtime/dist/server/run-server.js';
import { Awaiter } from '../awaiter.js';
const NODE_VERSION_MAJOR = process.version.match(/^v(\d+)\.\d+/)?.[1];
const NODE_VERSION_IDENTIFIER = `node${NODE_VERSION_MAJOR}`;
@@ -41,6 +48,8 @@ async function compileUserCode(
userCode: string;
wasmAssets: WasmAssets;
nodeCompatBindings: NodeCompatBindings;
entrypointPath: string;
awaiter: Awaiter;
}
> {
const { wasmAssets, plugin: edgeWasmPlugin } = createEdgeWasmPlugin();
@@ -110,9 +119,11 @@ async function compileUserCode(
`;
return {
entrypointPath: entrypointFullPath,
userCode,
wasmAssets,
nodeCompatBindings: nodeCompatPlugin.bindings,
awaiter: new Awaiter(),
};
} catch (error: unknown) {
// We can't easily show a meaningful stack trace from esbuild -> edge-runtime.
@@ -127,6 +138,8 @@ async function createEdgeRuntimeServer(params?: {
userCode: string;
wasmAssets: WasmAssets;
nodeCompatBindings: NodeCompatBindings;
entrypointPath: string;
awaiter: Awaiter;
}): Promise<
{ server: EdgeRuntimeServer; onExit: () => Promise<void> } | undefined
> {
@@ -157,6 +170,12 @@ async function createEdgeRuntimeServer(params?: {
// These are the global bindings for WebAssembly module
...wasmBindings,
FetchEvent: class extends context.FetchEvent {
waitUntil = (promise: Promise<unknown>) => {
params!.awaiter.waitUntil(promise);
};
},
});
return context;
@@ -165,31 +184,25 @@ async function createEdgeRuntimeServer(params?: {
const server = await runServer({ runtime });
const onExit = async () => {
// When exiting this process, wait for the Edge Runtime server to finish
// all its work, especially waitUntil promises before exiting this process.
//
// Here we use a short timeout (10 seconds) to let the user know that
// it has a long-running waitUntil promise.
const WAIT_UNTIL_TIMEOUT = 10 * 1000;
const waitUntil = server.close();
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
console.warn(
`Edge Runtime server is still running after ${WAIT_UNTIL_TIMEOUT} ms` +
` (hint: do you have a long-running waitUntil() promise?)`
);
resolve();
}, WAIT_UNTIL_TIMEOUT);
// @ts-expect-error
runtime.context.globalThis[Symbol.for('@vercel/request-context')] = {
get: () => ({
waitUntil: params.awaiter.waitUntil.bind(params.awaiter),
}),
};
waitUntil
const onExit = () =>
new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
console.warn(waitUntilWarning(params.entrypointPath));
resolve();
}, WAIT_UNTIL_TIMEOUT_MS);
Promise.all([params.awaiter.awaiting(), server.close()])
.then(() => resolve())
.catch(reject)
.finally(() => {
clearTimeout(timeout);
});
.finally(() => clearTimeout(timeout));
});
};
return { server, onExit };
} catch (error: any) {

View File

@@ -15,6 +15,7 @@ export function forkDevServer(options: {
require_: NodeRequire;
entrypoint: string;
meta: Meta;
printLogs?: boolean;
/**
* A path to the dev-server path. This is used in tests.
@@ -59,9 +60,19 @@ export function forkDevServer(options: {
: undefined,
NODE_OPTIONS: nodeOptions,
}),
stdio: options.printLogs ? 'pipe' : undefined,
};
const child = fork(devServerPath, [], forkOptions);
if (options.printLogs) {
child.stdout?.on('data', data => {
console.log(`stdout: ${data}`);
});
child.stderr?.on('data', data => {
console.error(`stderr: ${data}`);
});
}
checkForPid(devServerPath, child);
return child;

View File

@@ -1,44 +1,51 @@
import type { ServerResponse, IncomingMessage } from 'http';
import type { NodeHandler } from '@edge-runtime/node-utils';
import type { Awaiter } from '../awaiter';
import { buildToNodeHandler } from '@edge-runtime/node-utils';
import Edge from '@edge-runtime/primitives';
const webHandlerToNodeHandler = buildToNodeHandler(
{
Headers,
ReadableStream,
// @ts-expect-error Property 'duplex' is missing in type 'Request'
Request: class extends Request {
constructor(input: RequestInfo | URL, init?: RequestInit | undefined) {
super(input, addDuplexToInit(init));
}
export const createWebExportsHandler = (awaiter: Awaiter) => {
const webHandlerToNodeHandler = buildToNodeHandler(
{
Headers,
ReadableStream,
// @ts-expect-error Property 'duplex' is missing in type 'Request'
Request: class extends Request {
constructor(input: RequestInfo | URL, init?: RequestInit | undefined) {
super(input, addDuplexToInit(init));
}
},
Uint8Array,
// @ts-expect-error Property 'waitUntil' is missing in type 'FetchEvent'
FetchEvent: class {
waitUntil = (promise: Promise<unknown>) => awaiter.waitUntil(promise);
},
},
Uint8Array,
FetchEvent: Edge.FetchEvent,
},
{ defaultOrigin: 'https://vercel.com' }
);
{ defaultOrigin: 'https://vercel.com' }
);
/**
* When users export at least one HTTP handler, we will generate
* a generic handler routing to the right method. If there is no
* handler function exported returns null.
*/
export function getWebExportsHandler(listener: any, methods: string[]) {
const handlerByMethod: { [key: string]: NodeHandler } = {};
/**
* When users export at least one HTTP handler, we will generate
* a generic handler routing to the right method. If there is no
* handler function exported returns null.
*/
function getWebExportsHandler(listener: any, methods: string[]) {
const handlerByMethod: { [key: string]: NodeHandler } = {};
for (const key of methods) {
handlerByMethod[key] =
typeof listener[key] !== 'undefined'
? webHandlerToNodeHandler(listener[key])
: defaultHttpHandler;
for (const key of methods) {
handlerByMethod[key] =
typeof listener[key] !== 'undefined'
? webHandlerToNodeHandler(listener[key])
: defaultHttpHandler;
}
return (req: IncomingMessage, res: ServerResponse) => {
const method = req.method ?? 'GET';
handlerByMethod[method](req, res);
};
}
return (req: IncomingMessage, res: ServerResponse) => {
const method = req.method ?? 'GET';
handlerByMethod[method](req, res);
};
}
return getWebExportsHandler;
};
/**
* Add `duplex: 'half'` by default to all requests

View File

@@ -1,15 +1,21 @@
import { addHelpers } from './helpers.js';
import { createServer } from 'http';
import { serializeBody } from '../utils.js';
import {
WAIT_UNTIL_TIMEOUT_MS,
waitUntilWarning,
serializeBody,
} from '../utils.js';
import { type Dispatcher, Headers, request as undiciRequest } from 'undici';
import { listen } from 'async-listen';
import { isAbsolute } from 'path';
import { pathToFileURL } from 'url';
import { buildToHeaders } from '@edge-runtime/node-utils';
import { promisify } from 'util';
import type { ServerResponse, IncomingMessage } from 'http';
import type { VercelProxyResponse } from '../types.js';
import type { VercelRequest, VercelResponse } from './helpers.js';
import type { Readable } from 'stream';
import { Awaiter } from '../awaiter.js';
// @ts-expect-error
const toHeaders = buildToHeaders({ Headers });
@@ -43,14 +49,13 @@ async function createServerlessServer(
const server = createServer(userCode);
return {
url: await listen(server, { host: '127.0.0.1', port: 0 }),
onExit: async () => {
server.close();
},
onExit: promisify(server.close.bind(server)),
};
}
async function compileUserCode(
entrypointPath: string,
awaiter: Awaiter,
options: ServerlessServerOptions
) {
const id = isAbsolute(entrypointPath)
@@ -71,7 +76,8 @@ async function compileUserCode(
'Node.js v18 or above is required to use HTTP method exports in your functions.'
);
}
const { getWebExportsHandler } = await import('./helpers-web.js');
const { createWebExportsHandler } = await import('./helpers-web.js');
const getWebExportsHandler = createWebExportsHandler(awaiter);
return getWebExportsHandler(listener, HTTP_METHODS);
}
@@ -88,7 +94,19 @@ export async function createServerlessEventHandler(
handler: (request: IncomingMessage) => Promise<VercelProxyResponse>;
onExit: () => Promise<void>;
}> {
const userCode = await compileUserCode(entrypointPath, options);
const awaiter = new Awaiter();
Object.defineProperty(globalThis, Symbol.for('@vercel/request-context'), {
enumerable: false,
configurable: true,
value: {
get: () => ({
waitUntil: awaiter.waitUntil.bind(awaiter),
}),
},
});
const userCode = await compileUserCode(entrypointPath, awaiter, options);
const server = await createServerlessServer(userCode);
const isStreaming = options.mode === 'streaming';
@@ -122,8 +140,21 @@ export async function createServerlessEventHandler(
};
};
const onExit = () =>
new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
console.warn(waitUntilWarning(entrypointPath));
resolve();
}, WAIT_UNTIL_TIMEOUT_MS);
Promise.all([awaiter.awaiting(), server.onExit()])
.then(() => resolve())
.catch(reject)
.finally(() => clearTimeout(timeout));
});
return {
handler,
onExit: server.onExit,
onExit,
};
}

View File

@@ -3,6 +3,22 @@ import { pathToRegexp } from 'path-to-regexp';
import type { IncomingMessage } from 'http';
import { extname } from 'path';
// When exiting this process, wait for Vercel Function server to finish
// all its work, especially waitUntil promises before exiting this process.
//
// Here we use a short timeout (10 seconds) to let the user know that
// it has a long-running waitUntil promise.
const WAIT_UNTIL_TIMEOUT = 10;
export const WAIT_UNTIL_TIMEOUT_MS = 10 * 1000;
export const waitUntilWarning = (entrypointPath: string) =>
`
The function \`${entrypointPath
.split('/')
.pop()}\` is still running after ${WAIT_UNTIL_TIMEOUT}s.
(hint: do you have a long-running waitUntil() promise?)
`.trim();
export function getRegExpFromMatchers(matcherOrMatchers: unknown): string {
if (!matcherOrMatchers) {
return '^/.*$';

View File

@@ -0,0 +1,12 @@
const baseUrl = ({ headers }) =>
`${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`;
export function GET(request, ctx) {
const { searchParams } = new URL(request.url, baseUrl(request));
const url = searchParams.get('url');
ctx.waitUntil(fetch(url));
return Response.json({ keys: Object.keys(ctx) });
}
export const config = { runtime: 'edge' };

View File

@@ -0,0 +1,12 @@
function lazyError() {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('oh no'));
}, 100);
});
}
export function GET(_, ctx) {
ctx.waitUntil(lazyError());
return Response.json({ keys: Object.keys(ctx) });
}

View File

@@ -0,0 +1,10 @@
const baseUrl = ({ headers }) =>
`${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`;
export function GET(request, ctx) {
const { searchParams } = new URL(request.url, baseUrl(request));
const url = searchParams.get('url');
ctx.waitUntil(fetch(url));
return Response.json({ keys: Object.keys(ctx) });
}

View File

@@ -0,0 +1,13 @@
import { waitUntil } from '@vercel/functions';
const baseUrl = ({ headers }) =>
`${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`;
export function GET(request) {
const { searchParams } = new URL(request.url, baseUrl(request));
const url = searchParams.get('url');
waitUntil(fetch(url));
return new Response('OK');
}
export const config = { runtime: 'edge' };

View File

@@ -0,0 +1,11 @@
import { waitUntil } from '@vercel/functions';
const baseUrl = ({ headers }) =>
`${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`;
export function GET(request) {
const { searchParams } = new URL(request.url, baseUrl(request));
const url = searchParams.get('url');
waitUntil(fetch(url));
return new Response('OK');
}

View File

@@ -1,9 +1,11 @@
import { forkDevServer, readMessage } from '../../src/fork-dev-server';
import { resolve, extname } from 'path';
import { fetch } from 'undici';
import { createServer } from 'http';
import { listen } from 'async-listen';
import zlib from 'zlib';
import { once } from 'node:events';
import { fetch } from 'undici';
import { promisify } from 'util';
import { setTimeout } from 'timers/promises';
jest.setTimeout(20 * 1000);
@@ -29,23 +31,97 @@ function testForkDevServer(entrypoint: string) {
});
}
(NODE_MAJOR < 18 ? test.skip : test)(
'web handlers for node runtime',
async () => {
const child = testForkDevServer('./web-handlers-node.js');
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const teardown: any = [];
afterAll(() => Promise.all(teardown.map((fn: any) => fn())));
const { address, port } = result.value;
async function withHttpServer(hander: (req: any, res: any) => void) {
const server = createServer(hander);
teardown.push(promisify(server.close.bind(server)));
const address = await listen(server, { port: 0, host: '127.0.0.1' });
return address.toString();
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-node`,
{ method: 'GET' }
);
async function withDevServer(
entrypoint: string,
fn: (url: string) => Promise<void>,
{ runningTimeout }: { runningTimeout?: number } = {}
) {
const child = testForkDevServer(entrypoint);
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const { address, port } = result.value;
const url = `http://${address}:${port}`;
const start = Date.now();
try {
return await fn(url);
} finally {
const elapsed = Date.now() - start;
if (runningTimeout) await setTimeout(runningTimeout - elapsed);
child.send('shutdown', error => error && child.kill(9));
if (child.exitCode === null) await once(child, 'exit');
}
}
(NODE_MAJOR < 18 ? describe.skip : describe)('web handlers', () => {
describe('for node runtime', () => {
test('with `waitUntil` from import', () =>
withDevServer(
'./wait-until-node.js',
async (url: string) => {
let isWaitUntilCalled = false;
const serverUrl = await withHttpServer((_, res) => {
isWaitUntilCalled = true;
res.end();
});
await fetch(`${url}/api/wait-until-node?url=${serverUrl}`);
await setTimeout(50); // wait a bit for waitUntil resolution
expect(isWaitUntilCalled).toBe(true);
},
{ runningTimeout: 300 }
));
test('with `waitUntil` from context', () =>
withDevServer(
'./wait-until-ctx-node.js',
async (url: string) => {
let isWaitUntilCalled = false;
const serverUrl = await withHttpServer((_, res) => {
isWaitUntilCalled = true;
res.end();
});
const response = await fetch(
`${url}/api/wait-until-ctx-node?url=${serverUrl}`
);
expect(await response.json()).toEqual({ keys: ['waitUntil'] });
await setTimeout(50); // wait a bit for waitUntil resolution
expect(isWaitUntilCalled).toBe(true);
},
{ runningTimeout: 300 }
));
test('with `waitUntil` from context rejecting a promise ', () =>
withDevServer(
'./wait-until-ctx-node-rejected.js',
async (url: string) => {
const response = await fetch(
`${url}/api/wait-until-ctx-node-rejected`
);
await setTimeout(100); // wait a bit for waitUntil resolution
expect(response.status).toBe(200);
},
{ runningTimeout: 100 }
));
test('exporting GET', () =>
withDevServer('./web-handlers-node.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'GET',
});
expect({
status: response.status,
body: await response.text(),
@@ -57,12 +133,13 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using GET',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-node`,
{ method: 'POST' }
);
}));
test('exporting POST', () =>
withDevServer('./web-handlers-node.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'POST',
});
expect({
status: response.status,
body: await response.text(),
@@ -74,12 +151,13 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using POST',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-node`,
{ method: 'DELETE' }
);
}));
test('exporting DELETE', () =>
withDevServer('./web-handlers-node.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'DELETE',
});
expect({
status: response.status,
body: await response.text(),
@@ -91,12 +169,13 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using DELETE',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-node`,
{ method: 'PUT' }
);
}));
test('exporting PUT', () =>
withDevServer('./web-handlers-node.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'PUT',
});
expect({
status: response.status,
body: await response.text(),
@@ -108,12 +187,13 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using PUT',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-node`,
{ method: 'PATCH' }
);
}));
test('exporting PATCH', () =>
withDevServer('./web-handlers-node.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'PATCH',
});
expect({
status: response.status,
body: await response.text(),
@@ -125,25 +205,31 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using PATCH',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-node`,
{ method: 'HEAD' }
);
}));
test('exporting HEAD', () =>
withDevServer('./web-handlers-node.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'HEAD',
});
expect({
status: response.status,
body: await response.text(),
transferEncoding: response.headers.get('transfer-encoding'),
'x-web-handler': response.headers.get('x-web-handler'),
}).toEqual({
status: 200,
body: '',
transferEncoding: null,
'x-web-handler': 'Web handler using HEAD',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-node`,
{ method: 'OPTIONS' }
);
}));
test('exporting OPTIONS', () =>
withDevServer('./web-handlers-node.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'OPTIONS',
});
expect({
status: response.status,
body: await response.text(),
@@ -155,30 +241,140 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using OPTIONS',
});
}
} finally {
child.kill(9);
}
}
);
}));
});
(NODE_MAJOR < 18 ? test.skip : test)(
'web handlers for edge runtime',
async () => {
const child = testForkDevServer('./web-handlers-edge.js');
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
describe('for edge runtime', () => {
test('with `waitUntil` from import', () =>
withDevServer(
'./wait-until-edge.js',
async (url: string) => {
let isWaitUntilCalled = false;
const serverUrl = await withHttpServer((_, res) => {
isWaitUntilCalled = true;
res.end();
});
await fetch(`${url}/api/wait-until-edge?url=${serverUrl}`);
await setTimeout(50); // wait a bit for waitUntil resolution
expect(isWaitUntilCalled).toBe(true);
},
{ runningTimeout: 300 }
));
const { address, port } = result.value;
test('with `waitUntil` from context', () =>
withDevServer(
'./wait-until-ctx-edge.js',
async (url: string) => {
let isWaitUntilCalled = false;
const serverUrl = await withHttpServer((_, res) => {
isWaitUntilCalled = true;
res.end();
});
const response = await fetch(
`${url}/api/wait-until-ctx-edge?url=${serverUrl}`
);
expect(await response.json()).toEqual({ keys: ['waitUntil'] });
await setTimeout(50); // wait a bit for waitUntil resolution
expect(isWaitUntilCalled).toBe(true);
},
{ runningTimeout: 300 }
));
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-edge`,
{ method: 'GET' }
);
test("user code doesn't interfere with runtime", () =>
withDevServer('./edge-self.js', async (url: string) => {
const response = await fetch(`${url}/api/edge-self`);
expect({
status: response.status,
}).toEqual({
status: 200,
});
}));
test('with `WebSocket`', () =>
withDevServer('./edge-websocket.js', async (url: string) => {
const response = await fetch(`${url}/api/edge-websocket`);
expect({
status: response.status,
body: await response.text(),
}).toEqual({
status: 200,
body: '3210',
});
}));
test('with `Buffer`', () =>
withDevServer('./edge-buffer.js', async (url: string) => {
const response = await fetch(`${url}/api/edge-buffer`);
expect({
status: response.status,
json: await response.json(),
}).toEqual({
status: 200,
json: {
encoded: Buffer.from('Hello, world!').toString('base64'),
'Buffer === B.Buffer': true,
},
});
}));
test('runs a mjs endpoint', () =>
withDevServer('./esm-module.mjs', async (url: string) => {
const response = await fetch(`${url}/api/hello`);
expect({
status: response.status,
headers: Object.fromEntries(response.headers),
text: await response.text(),
}).toEqual({
status: 200,
headers: expect.objectContaining({
'x-hello': 'world',
}),
text: 'Hello, world!',
});
}));
(process.platform === 'win32' ? test.skip : test)(
'runs a esm typescript endpoint',
() =>
withDevServer('./esm-module.ts', async (url: string) => {
const response = await fetch(`${url}/api/hello`);
expect({
status: response.status,
headers: Object.fromEntries(response.headers),
text: await response.text(),
}).toEqual({
status: 200,
headers: expect.objectContaining({
'x-hello': 'world',
}),
text: 'Hello, world!',
});
})
);
(process.platform === 'win32' ? test.skip : test)(
'allow setting multiple cookies with same name',
() =>
withDevServer('./multiple-cookies.ts', async (url: string) => {
const response = await fetch(`${url}/api/hello`, { method: 'GET' });
expect({
status: response.status,
text: await response.text(),
}).toEqual({
status: 200,
text: 'Hello, world!',
});
expect(response.headers.getSetCookie()).toEqual([
'a=x',
'b=y',
'c=z',
]);
})
);
test('exporting GET', () =>
withDevServer('./web-handlers-edge.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'GET',
});
expect({
status: response.status,
body: await response.text(),
@@ -190,12 +386,13 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using GET',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-edge`,
{ method: 'POST' }
);
}));
test('exporting POST', () =>
withDevServer('./web-handlers-edge.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'POST',
});
expect({
status: response.status,
body: await response.text(),
@@ -207,29 +404,31 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using POST',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-edge`,
{ method: 'DELETE' }
);
}));
console.log(response);
test('exporting DELETE', () =>
withDevServer('./web-handlers-edge.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'DELETE',
});
expect({
status: response.status,
body: await response.text(),
transferEncoding: response.headers.get('transfer-encoding'),
'x-web-handler': response.headers.get('x-web-handler'),
}).toEqual({
status: 200,
body: 'Web handler using DELETE',
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using DELETE',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-edge`,
{ method: 'PUT' }
);
}));
test('exporting PUT', () =>
withDevServer('./web-handlers-edge.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'PUT',
});
expect({
status: response.status,
body: await response.text(),
@@ -241,12 +440,13 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using PUT',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-edge`,
{ method: 'PATCH' }
);
}));
test('exporting PATCH', () =>
withDevServer('./web-handlers-edge.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'PATCH',
});
expect({
status: response.status,
body: await response.text(),
@@ -258,25 +458,31 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using PATCH',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-edge`,
{ method: 'HEAD' }
);
}));
test('exporting HEAD', () =>
withDevServer('./web-handlers-edge.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'HEAD',
});
expect({
status: response.status,
body: await response.text(),
transferEncoding: response.headers.get('transfer-encoding'),
'x-web-handler': response.headers.get('x-web-handler'),
}).toEqual({
status: 200,
body: '',
transferEncoding: null,
'x-web-handler': 'Web handler using HEAD',
});
}
{
const response = await fetch(
`http://${address}:${port}/api/web-handlers-edge`,
{ method: 'OPTIONS' }
);
}));
test('exporting OPTIONS', () =>
withDevServer('./web-handlers-edge.js', async (url: string) => {
const response = await fetch(`${url}/api/web-handlers-node`, {
method: 'OPTIONS',
});
expect({
status: response.status,
body: await response.text(),
@@ -288,338 +494,6 @@ function testForkDevServer(entrypoint: string) {
transferEncoding: 'chunked',
'x-web-handler': 'Web handler using OPTIONS',
});
}
} finally {
child.kill(9);
}
}
);
(NODE_MAJOR < 18 ? test.skip : test)(
'buffer fetch response correctly',
async () => {
const child = testForkDevServer('./serverless-fetch.js');
const server = createServer((req, res) => {
res.setHeader('Content-Encoding', 'br');
const searchParams = new URLSearchParams(req.url!.substring(1));
const encoding = searchParams.get('encoding') ?? 'identity';
res.writeHead(200, {
'content-type': 'text/plain',
'content-encoding': encoding,
});
let payload = Buffer.from('Greetings, Vercel');
if (encoding === 'br') {
payload = zlib.brotliCompressSync(payload, {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 0,
},
});
} else if (encoding === 'gzip') {
payload = zlib.gzipSync(payload, {
level: zlib.constants.Z_BEST_SPEED,
});
} else if (encoding === 'deflate') {
payload = zlib.deflateSync(payload, {
level: zlib.constants.Z_BEST_SPEED,
});
}
res.end(payload);
});
const serverUrl = (await listen(server)).toString();
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const { address, port } = result.value;
{
const response = await fetch(
`http://${address}:${port}/api/serverless-fetch?url=${serverUrl}&encoding=br`
);
expect(response.headers.get('content-encoding')).toBe('br');
expect(response.headers.get('content-length')).toBe('21');
expect({
status: response.status,
body: await response.text(),
}).toEqual({ status: 200, body: 'Greetings, Vercel' });
}
{
const response = await fetch(
`http://${address}:${port}/api/serverless-fetch?url=${serverUrl}&encoding=gzip`
);
expect(response.headers.get('content-encoding')).toBe('gzip');
expect(response.headers.get('content-length')).toBe('37');
expect({
status: response.status,
body: await response.text(),
}).toEqual({ status: 200, body: 'Greetings, Vercel' });
}
{
const response = await fetch(
`http://${address}:${port}/api/serverless-fetch?url=${serverUrl}&encoding=deflate`
);
expect(response.headers.get('content-encoding')).toBe('deflate');
expect(response.headers.get('content-length')).toBe('25');
expect({
status: response.status,
body: await response.text(),
}).toEqual({ status: 200, body: 'Greetings, Vercel' });
}
} finally {
server.close();
child.kill(9);
}
}
);
test("user code doesn't interfere with runtime", async () => {
const child = testForkDevServer('./edge-self.js');
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const { address, port } = result.value;
const response = await fetch(`http://${address}:${port}/api/edge-self`);
expect({
status: response.status,
}).toEqual({
status: 200,
});
} finally {
child.kill(9);
}
});
test('runs an edge function that uses `WebSocket`', async () => {
const child = testForkDevServer('./edge-websocket.js');
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const { address, port } = result.value;
const response = await fetch(
`http://${address}:${port}/api/edge-websocket`
);
expect({
status: response.status,
body: await response.text(),
}).toEqual({
status: 200,
body: '3210',
});
} finally {
child.kill(9);
}
});
test('runs an edge function that uses `buffer`', async () => {
const child = testForkDevServer('./edge-buffer.js');
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const { address, port } = result.value;
const response = await fetch(`http://${address}:${port}/api/edge-buffer`);
expect({
status: response.status,
json: await response.json(),
}).toEqual({
status: 200,
json: {
encoded: Buffer.from('Hello, world!').toString('base64'),
'Buffer === B.Buffer': true,
},
});
} finally {
child.kill(9);
}
});
test('runs a mjs endpoint', async () => {
const child = testForkDevServer('./esm-module.mjs');
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const { address, port } = result.value;
const response = await fetch(`http://${address}:${port}/api/hello`);
expect({
status: response.status,
headers: Object.fromEntries(response.headers),
text: await response.text(),
}).toEqual({
status: 200,
headers: expect.objectContaining({
'x-hello': 'world',
}),
text: 'Hello, world!',
});
} finally {
child.kill(9);
}
});
test('runs a esm typescript endpoint', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on Windows');
return;
}
const child = testForkDevServer('./esm-module.ts');
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const { address, port } = result.value;
const response = await fetch(`http://${address}:${port}/api/hello`);
expect({
status: response.status,
headers: Object.fromEntries(response.headers),
text: await response.text(),
}).toEqual({
status: 200,
headers: expect.objectContaining({
'x-hello': 'world',
}),
text: 'Hello, world!',
});
} finally {
child.kill(9);
}
});
test('allow setting multiple cookies with same name', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on Windows');
return;
}
const child = testForkDevServer('./multiple-cookies.ts');
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error(`Exited. error: ${JSON.stringify(result.value)}`);
}
const { address, port } = result.value;
const response = await fetch(`http://${address}:${port}/api/hello`);
expect({
status: response.status,
text: await response.text(),
}).toEqual({
status: 200,
text: 'Hello, world!',
});
expect(response.headers.getSetCookie()).toEqual(['a=x', 'b=y', 'c=z']);
} finally {
child.kill(9);
}
});
test('dev server waits for waitUntil promises to resolve', async () => {
async function startPingServer() {
let resolve: (value: any) => void;
const promise = new Promise<void>(resolve_ => {
resolve = resolve_;
});
const pingServer = createServer((req, res) => {
res.end('pong');
resolve('got a fetch from waitUntil');
});
const pingUrl = (await listen(pingServer)).toString();
return {
pingUrl,
pingServer,
promise,
};
}
async function withTimeout(
promise: Promise<unknown>,
name: string,
ms: number
) {
return await Promise.race([
promise,
new Promise(resolve =>
setTimeout(
() => resolve(`${name} promise was not resolved in ${ms} ms`),
ms
)
),
]);
}
const { promise: pingPromise, pingServer, pingUrl } = await startPingServer();
const child = testForkDevServer('./edge-waituntil.js');
const exitPromise = new Promise(resolve => {
child.on('exit', code => {
resolve(`child has exited with ${code}`);
});
}));
});
try {
const result = await readMessage(child);
if (result.state !== 'message') {
throw new Error('Exited. error: ' + JSON.stringify(result.value));
}
const { address, port } = result.value;
const response = await fetch(
`http://${address}:${port}/api/edge-waituntil`,
{
headers: {
'x-ping-url': pingUrl,
},
}
);
expect({
status: response.status,
body: await response.text(),
}).toEqual({
status: 200,
body: 'running waitUntil promises asynchronously...',
});
// Dev server should keep running until waitUntil promise resolves...
child.send('shutdown');
// Wait for waitUntil promise to resolve...
expect(await withTimeout(pingPromise, 'ping server', 3000)).toBe(
'got a fetch from waitUntil'
);
// Make sure child process has exited.
expect(await withTimeout(exitPromise, 'child exit', 5000)).toBe(
'child has exited with 0'
);
} finally {
child.kill(9);
pingServer.close();
}
});

View File

@@ -1,5 +1,17 @@
# @vercel/static-build
## 2.5.3
### Patch Changes
- Support Ruby and Python static site generators in AL2023 build image ([#11529](https://github.com/vercel/vercel/pull/11529))
## 2.5.2
### Patch Changes
- Revert `SUPPORTED_RUBY_VERSION` change to fix Ruby site generators on AL2 build image ([#11522](https://github.com/vercel/vercel/pull/11522))
## 2.5.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-build",
"version": "2.5.1",
"version": "2.5.3",
"license": "Apache-2.0",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/build-step",

View File

@@ -4,7 +4,12 @@ import fetch from 'node-fetch';
import getPort from 'get-port';
import isPortReachable from 'is-port-reachable';
import frameworks, { Framework } from '@vercel/frameworks';
import { spawn, type ChildProcess, type SpawnOptions } from 'child_process';
import {
spawn,
spawnSync,
type ChildProcess,
type SpawnOptions,
} from 'child_process';
import { existsSync, readFileSync, statSync, readdirSync, mkdirSync } from 'fs';
import { cpus } from 'os';
import {
@@ -48,7 +53,6 @@ import {
import { getHugoUrl } from './utils/hugo';
import { once } from 'events';
const SUPPORTED_RUBY_VERSION = '3.3.0';
const sleep = (n: number) => new Promise(resolve => setTimeout(resolve, n));
const DEV_SERVER_PORT_BIND_TIMEOUT = ms('5m');
@@ -579,9 +583,13 @@ export const build: BuildV2 = async ({
pathList.push(vendorBin); // Add `./vendor/bin`
debug(`Added "${vendorBin}" to PATH env because a Gemfile was found`);
const dir = path.join(workPath, 'vendor', 'bundle', 'ruby');
const rubyVersion = SUPPORTED_RUBY_VERSION;
const rubyVersion = spawnSync(
'ruby',
['-e', 'print "#{ RUBY_VERSION }"'],
{ encoding: 'utf8' }
);
if (rubyVersion) {
gemHome = path.join(dir, rubyVersion);
gemHome = path.join(dir, rubyVersion.stdout.trim());
debug(`Set GEM_HOME="${gemHome}" because a Gemfile was found`);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,9 @@
"prettier": "@shopify/prettier-config",
"dependencies": {
"@remix-run/react": "1.19.1",
"@shopify/cli": "3.59.1",
"@shopify/cli-hydrogen": "^5.5.2",
"@shopify/hydrogen": "^2023.10.6",
"@shopify/cli": "3.49.2",
"@shopify/cli-hydrogen": "^5.4.1",
"@shopify/hydrogen": "^2023.7.9",
"@shopify/remix-oxygen": "^1.1.5",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",

View File

@@ -14,7 +14,7 @@
"@angular/core": "^16.0.0",
"@angular/platform-browser": "^16.0.0",
"@angular/platform-browser-dynamic": "^16.2.12",
"@ionic/angular": "^8.0.0",
"@ionic/angular": "^8.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.6.2",
"zone.js": "^0.13.3"
@@ -3000,11 +3000,11 @@
"peer": true
},
"node_modules/@ionic/angular": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.0.1.tgz",
"integrity": "sha512-VqGsntNXPpDemzfPv2U28fgP5+DTcElatmQfcHANlG61f4t6/QJ/1IweNph0I4gZddieZwW3LF5ag4BAmGRhzw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.1.0.tgz",
"integrity": "sha512-/cl5+C9WW+8dH2Sfi92SBFpxGAubRhVJC2TnsNcJYBmWw9fUDV7DfrUplXshopDoqouOFMYvelbvmrqigGyxwg==",
"dependencies": {
"@ionic/core": "8.0.1",
"@ionic/core": "8.1.0",
"ionicons": "^7.0.0",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
@@ -3119,11 +3119,11 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.0.1.tgz",
"integrity": "sha512-6FEa0kSXSs82aCYcB7JcLGt5Z0XBU8mRFQGVrJtdh3ybQQntIAWKHc9H2OFaiT3SSAK+XQqlU6kq0jM9nWKveQ==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.1.0.tgz",
"integrity": "sha512-CTa0JZA7T0Je7HiAinj/kjpxChQYDvitFBqMtNv88nOJn1KerbUKmV2JfQ0quNFneN8z/bBdNOaKc8T++cyDyg==",
"dependencies": {
"@stencil/core": "^4.17.1",
"@stencil/core": "^4.17.2",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
}
@@ -3924,9 +3924,9 @@
}
},
"node_modules/@stencil/core": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.17.1.tgz",
"integrity": "sha512-nlARe1QtK5abnCG8kPQKJMWiELg39vKabvf3ebm6YEhQA35CgrxC1pVYTsYq3yktJKoY+k+VzGRnATLKyaLbvA==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.17.2.tgz",
"integrity": "sha512-MX7yaLmpTU9iZvCire9nhecTcE0qBlV0vPWrLMeIXewYN7/hb8B3NjnhQyBKC93FDPI8NBRmt6KIugLw9zcRZg==",
"bin": {
"stencil": "bin/stencil"
},
@@ -8171,9 +8171,9 @@
}
},
"node_modules/ionicons": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.3.1.tgz",
"integrity": "sha512-1boG4EQTBBpQ4/0PU60Yi78Iw/k8iNtKu9c0NmsbzHGnWAcwpiovG9Wi/rk5UlF+DC+CR4XDCxKo91YqvAxkww==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"dependencies": {
"@stencil/core": "^4.0.3"
}
@@ -15842,11 +15842,11 @@
"peer": true
},
"@ionic/angular": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.0.1.tgz",
"integrity": "sha512-VqGsntNXPpDemzfPv2U28fgP5+DTcElatmQfcHANlG61f4t6/QJ/1IweNph0I4gZddieZwW3LF5ag4BAmGRhzw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.1.0.tgz",
"integrity": "sha512-/cl5+C9WW+8dH2Sfi92SBFpxGAubRhVJC2TnsNcJYBmWw9fUDV7DfrUplXshopDoqouOFMYvelbvmrqigGyxwg==",
"requires": {
"@ionic/core": "8.0.1",
"@ionic/core": "8.1.0",
"ionicons": "^7.0.0",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
@@ -15927,11 +15927,11 @@
}
},
"@ionic/core": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.0.1.tgz",
"integrity": "sha512-6FEa0kSXSs82aCYcB7JcLGt5Z0XBU8mRFQGVrJtdh3ybQQntIAWKHc9H2OFaiT3SSAK+XQqlU6kq0jM9nWKveQ==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.1.0.tgz",
"integrity": "sha512-CTa0JZA7T0Je7HiAinj/kjpxChQYDvitFBqMtNv88nOJn1KerbUKmV2JfQ0quNFneN8z/bBdNOaKc8T++cyDyg==",
"requires": {
"@stencil/core": "^4.17.1",
"@stencil/core": "^4.17.2",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
}
@@ -16481,9 +16481,9 @@
}
},
"@stencil/core": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.17.1.tgz",
"integrity": "sha512-nlARe1QtK5abnCG8kPQKJMWiELg39vKabvf3ebm6YEhQA35CgrxC1pVYTsYq3yktJKoY+k+VzGRnATLKyaLbvA=="
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.17.2.tgz",
"integrity": "sha512-MX7yaLmpTU9iZvCire9nhecTcE0qBlV0vPWrLMeIXewYN7/hb8B3NjnhQyBKC93FDPI8NBRmt6KIugLw9zcRZg=="
},
"@tootallnate/once": {
"version": "1.1.2",
@@ -19760,9 +19760,9 @@
}
},
"ionicons": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.3.1.tgz",
"integrity": "sha512-1boG4EQTBBpQ4/0PU60Yi78Iw/k8iNtKu9c0NmsbzHGnWAcwpiovG9Wi/rk5UlF+DC+CR4XDCxKo91YqvAxkww==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"requires": {
"@stencil/core": "^4.0.3"
}

View File

@@ -17,7 +17,7 @@
"@angular/core": "^16.0.0",
"@angular/platform-browser": "^16.0.0",
"@angular/platform-browser-dynamic": "^16.2.12",
"@ionic/angular": "^8.0.0",
"@ionic/angular": "^8.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.6.2",
"zone.js": "^0.13.3"

View File

@@ -0,0 +1,3 @@
_site
.sass-cache
.jekyll-metadata

View File

@@ -0,0 +1,24 @@
---
layout: default
---
<style type="text/css" media="screen">
.container {
margin: 10px auto;
max-width: 600px;
text-align: center;
}
h1 {
margin: 30px 0;
font-size: 4em;
line-height: 1;
letter-spacing: -1px;
}
</style>
<div class="container">
<h1>404</h1>
<p><strong>Page not found :(</strong></p>
<p>The requested page could not be found.</p>
</div>

View File

@@ -0,0 +1,34 @@
source "https://rubygems.org"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> 4.0"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "minima", "~> 2.0"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
# gem "github-pages", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.6"
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
gem "tzinfo", "~> 1.2"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?

View File

@@ -0,0 +1,89 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
colorator (1.1.0)
concurrent-ruby (1.2.2)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
eventmachine (1.2.7)
ffi (1.16.3)
forwardable-extended (2.6.0)
google-protobuf (3.24.4-arm64-darwin)
http_parser.rb (0.8.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
jekyll (4.3.2)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 1.0)
jekyll-sass-converter (>= 2.0, < 4.0)
jekyll-watch (~> 2.0)
kramdown (~> 2.3, >= 2.3.1)
kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0)
mercenary (>= 0.3.6, < 0.5)
pathutil (~> 0.9)
rouge (>= 3.0, < 5.0)
safe_yaml (~> 1.0)
terminal-table (>= 1.8, < 4.0)
webrick (~> 1.7)
jekyll-feed (0.17.0)
jekyll (>= 3.7, < 5.0)
jekyll-sass-converter (3.0.0)
sass-embedded (~> 1.54)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (5.0.3)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.6)
rouge (4.1.3)
safe_yaml (1.0.5)
sass-embedded (1.68.0-arm64-darwin)
google-protobuf (~> 3.23)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
thread_safe (0.3.6)
tzinfo (1.2.11)
thread_safe (~> 0.1)
tzinfo-data (1.2023.3)
tzinfo (>= 1.0.0)
unicode-display_width (2.5.0)
wdm (0.1.1)
webrick (1.8.1)
PLATFORMS
arm64-darwin-22
DEPENDENCIES
jekyll (~> 4.0)
jekyll-feed (~> 0.6)
minima (~> 2.0)
tzinfo (~> 1.2)
tzinfo-data
wdm (~> 0.1.0)
BUNDLED WITH
2.4.19

View File

@@ -0,0 +1,45 @@
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely edit after that. If you find
# yourself editing this file very often, consider using Jekyll's data files
# feature for the data you need to update frequently.
#
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
# Site settings
# These are used to personalize your new site. If you look in the HTML files,
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.
title: Your awesome title
email: your-email@example.com
description: >- # this means to ignore newlines until "baseurl:"
Write an awesome description for your new site here. You can edit this
line in _config.yml. It will appear in your document head meta (for
Google search results) and in your feed.xml site description.
baseurl: "" # the subpath of your site, e.g. /blog
url: "" # the base hostname & protocol for your site, e.g. http://example.com
twitter_username: jekyllrb
github_username: jekyll
# Build settings
markdown: kramdown
theme: minima
plugins:
- jekyll-feed
sass:
quiet_deps: true
# Exclude from processing.
# The following items will not be processed, by default. Create a custom list
# to override the default setting.
# exclude:
# - Gemfile
# - Gemfile.lock
# - node_modules
# - vendor/bundle/
# - vendor/cache/
# - vendor/gems/
# - vendor/ruby/

View File

@@ -0,0 +1,10 @@
{
"probes": [
{ "path": "/", "mustContain": "Your awesome title" },
{ "path": "/about/", "mustContain": "You can find out more info" },
{
"path": "/jekyll/update/2019/09/06/welcome-to-jekyll.html",
"mustContain": "Go ahead and edit it"
}
]
}

View File

@@ -1,3 +1,5 @@
_site
.sass-cache
.jekyll-cache
.jekyll-metadata
vendor

View File

@@ -1,4 +1,5 @@
---
permalink: /404.html
layout: default
---

View File

@@ -1,5 +1,4 @@
source "https://rubygems.org"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
@@ -8,27 +7,27 @@ source "https://rubygems.org"
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> 4.0"
gem "jekyll", "~> 4.3.3"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "minima", "~> 2.0"
gem "minima", "~> 2.5"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
# gem "github-pages", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.6"
gem "jekyll-feed", "~> 0.12"
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
gem "tzinfo", "~> 1.2"
platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo", ">= 1", "< 3"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
# do not have a Java counterpart.
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]

View File

@@ -1,21 +1,32 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.5)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
colorator (1.1.0)
concurrent-ruby (1.2.2)
concurrent-ruby (1.2.3)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
eventmachine (1.2.7)
ffi (1.16.3)
forwardable-extended (2.6.0)
google-protobuf (3.24.4-arm64-darwin)
google-protobuf (4.26.1)
rake (>= 13)
google-protobuf (4.26.1-aarch64-linux)
rake (>= 13)
google-protobuf (4.26.1-arm64-darwin)
rake (>= 13)
google-protobuf (4.26.1-x86-linux)
rake (>= 13)
google-protobuf (4.26.1-x86_64-darwin)
rake (>= 13)
google-protobuf (4.26.1-x86_64-linux)
rake (>= 13)
http_parser.rb (0.8.0)
i18n (1.14.1)
i18n (1.14.4)
concurrent-ruby (~> 1.0)
jekyll (4.3.2)
jekyll (4.3.3)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
@@ -44,7 +55,7 @@ GEM
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.8.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0)
@@ -54,36 +65,99 @@ GEM
jekyll-seo-tag (~> 2.1)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (5.0.3)
public_suffix (5.0.5)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.6)
rouge (4.1.3)
rouge (4.2.1)
safe_yaml (1.0.5)
sass-embedded (1.68.0-arm64-darwin)
google-protobuf (~> 3.23)
sass-embedded (1.76.0)
google-protobuf (>= 3.25, < 5.0)
rake (>= 13.0.0)
sass-embedded (1.76.0-aarch64-linux-android)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-aarch64-linux-gnu)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-aarch64-linux-musl)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-aarch64-mingw-ucrt)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-arm-linux-androideabi)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-arm-linux-gnueabihf)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-arm-linux-musleabihf)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-arm64-darwin)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-riscv64-linux-android)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-riscv64-linux-gnu)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-riscv64-linux-musl)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86-cygwin)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86-linux-android)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86-linux-gnu)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86-linux-musl)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86-mingw-ucrt)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86_64-cygwin)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86_64-darwin)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86_64-linux-android)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86_64-linux-gnu)
google-protobuf (>= 3.25, < 5.0)
sass-embedded (1.76.0-x86_64-linux-musl)
google-protobuf (>= 3.25, < 5.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
thread_safe (0.3.6)
tzinfo (1.2.11)
thread_safe (~> 0.1)
tzinfo-data (1.2023.3)
tzinfo (>= 1.0.0)
unicode-display_width (2.5.0)
wdm (0.1.1)
webrick (1.8.1)
PLATFORMS
arm64-darwin-22
aarch64-linux
aarch64-linux-android
aarch64-linux-gnu
aarch64-linux-musl
aarch64-mingw-ucrt
arm-linux-androideabi
arm-linux-gnueabihf
arm-linux-musleabihf
arm64-darwin
riscv64-linux-android
riscv64-linux-gnu
riscv64-linux-musl
ruby
x86-cygwin
x86-linux
x86-linux-android
x86-linux-gnu
x86-linux-musl
x86-mingw-ucrt
x86_64-cygwin
x86_64-darwin
x86_64-linux
x86_64-linux-android
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
jekyll (~> 4.0)
jekyll-feed (~> 0.6)
minima (~> 2.0)
tzinfo (~> 1.2)
http_parser.rb (~> 0.6.0)
jekyll (~> 4.3.3)
jekyll-feed (~> 0.12)
minima (~> 2.5)
tzinfo (>= 1, < 3)
tzinfo-data
wdm (~> 0.1.0)
wdm (~> 0.1.1)
BUNDLED WITH
2.4.19
2.5.9

View File

@@ -7,12 +7,17 @@
#
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
#
# If you need help with YAML syntax, here are some quick references for you:
# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
# https://learnxinyminutes.com/docs/yaml/
#
# Site settings
# These are used to personalize your new site. If you look in the HTML files,
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.
title: Your awesome title
email: your-email@example.com
description: >- # this means to ignore newlines until "baseurl:"
@@ -25,20 +30,25 @@ twitter_username: jekyllrb
github_username: jekyll
# Build settings
markdown: kramdown
theme: minima
plugins:
- jekyll-feed
sass:
quiet_deps: true
# Exclude from processing.
# The following items will not be processed, by default. Create a custom list
# to override the default setting.
# The following items will not be processed, by default.
# Any item listed under the `exclude:` key here will be automatically added to
# the internal "default list".
#
# Excluded items can be processed by explicitly listing the directories or
# their entries' file path in the `include:` list.
#
# exclude:
# - .sass-cache/
# - .jekyll-cache/
# - gemfiles/
# - Gemfile
# - Gemfile.lock
# - node_modules
# - node_modules/
# - vendor/bundle/
# - vendor/cache/
# - vendor/gems/

View File

@@ -0,0 +1,29 @@
---
layout: post
title: "Welcome to Jekyll!"
date: 2024-05-01 20:47:19 -0700
categories: jekyll update
---
Youll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated.
Jekyll requires blog post files to be named according to the following format:
`YEAR-MONTH-DAY-title.MARKUP`
Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works.
Jekyll also offers powerful support for code snippets:
{% highlight ruby %}
def print_hi(name)
puts "Hi, #{name}"
end
print_hi('Tom')
#=> prints 'Hi, Tom' to STDOUT.
{% endhighlight %}
Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekylls GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk].
[jekyll-docs]: https://jekyllrb.com/docs/home
[jekyll-gh]: https://github.com/jekyll/jekyll
[jekyll-talk]: https://talk.jekyllrb.com/

View File

@@ -0,0 +1,18 @@
---
layout: page
title: About
permalink: /about/
---
This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/)
You can find the source code for Minima at GitHub:
[jekyll][jekyll-organization] /
[minima](https://github.com/jekyll/minima)
You can find the source code for Jekyll at GitHub:
[jekyll][jekyll-organization] /
[jekyll](https://github.com/jekyll/jekyll)
[jekyll-organization]: https://github.com/jekyll

View File

@@ -0,0 +1,6 @@
---
# Feel free to add content and custom Front Matter to this file.
# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
layout: home
---

View File

@@ -3,7 +3,7 @@
{ "path": "/", "mustContain": "Your awesome title" },
{ "path": "/about/", "mustContain": "You can find out more info" },
{
"path": "/jekyll/update/2019/09/06/welcome-to-jekyll.html",
"path": "/jekyll/update/2024/05/02/welcome-to-jekyll.html",
"mustContain": "Go ahead and edit it"
}
]

View File

@@ -0,0 +1,5 @@
.bundle
.cache
.DS_Store
.sass-cache
build/

View File

@@ -0,0 +1,6 @@
source 'https://rubygems.org'
gem 'middleman', '~> 4.2'
gem 'middleman-autoprefixer', '~> 2.7'
gem 'tzinfo-data', platforms: [:mswin, :mingw, :jruby, :x64_mingw]
gem 'wdm', '~> 0.1', platforms: [:mswin, :mingw, :x64_mingw]

Some files were not shown because too many files have changed in this diff Show More