mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
41 Commits
amyegan-sv
...
@vercel/fu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fe92d7de0 | ||
|
|
0817527f9e | ||
|
|
6476f4f786 | ||
|
|
a5ea04154b | ||
|
|
5a532a5b94 | ||
|
|
c1d8522950 | ||
|
|
50fc27ba57 | ||
|
|
3101d2497f | ||
|
|
4b0716b556 | ||
|
|
124846a3e6 | ||
|
|
30cd0ec898 | ||
|
|
dc974b6797 | ||
|
|
58c6755e0c | ||
|
|
f18575fcd0 | ||
|
|
d6412be6bc | ||
|
|
52e435aa5d | ||
|
|
303256343a | ||
|
|
88f3a816d9 | ||
|
|
3420ba0153 | ||
|
|
3fb97d1d27 | ||
|
|
f11c7024c4 | ||
|
|
5eb4cc6f5d | ||
|
|
b1adaf76ec | ||
|
|
1bf04ba421 | ||
|
|
15475c8a2c | ||
|
|
1c8f0a4c33 | ||
|
|
ba65da97e1 | ||
|
|
28b6b810be | ||
|
|
9fc9a10c82 | ||
|
|
aef56edc35 | ||
|
|
e6a3497fb1 | ||
|
|
763a6d1070 | ||
|
|
21f5e7375e | ||
|
|
256d0fd463 | ||
|
|
5b79603378 | ||
|
|
863199aa0a | ||
|
|
97e02ae506 | ||
|
|
bd9236f228 | ||
|
|
26bc51e0d6 | ||
|
|
21b87d2ee6 | ||
|
|
f765c24689 |
@@ -1,2 +0,0 @@
|
||||
---
|
||||
---
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@vercel/next': patch
|
||||
---
|
||||
|
||||
[next] Reland add .action handling for dynamic routes
|
||||
@@ -1,2 +0,0 @@
|
||||
---
|
||||
---
|
||||
29
.github/dependabot.yml
vendored
29
.github/dependabot.yml
vendored
@@ -78,7 +78,7 @@ updates:
|
||||
update-types:
|
||||
- 'minor'
|
||||
|
||||
- directory: /packages/static-build/test/fixtures/ionic-angular-v7
|
||||
- directory: /packages/static-build/test/fixtures/ionic-angular-v8
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
open-pull-requests-limit: 1
|
||||
@@ -175,6 +175,33 @@ updates:
|
||||
update-types:
|
||||
- 'minor'
|
||||
|
||||
- directory: /packages/static-build/test/fixtures/parcel-v2
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
open-pull-requests-limit: 1
|
||||
reviewers:
|
||||
- 'trek'
|
||||
- 'TooTallNate'
|
||||
- 'EndangeredMassa'
|
||||
- 'erikareads'
|
||||
- 'jeffsee55'
|
||||
- 'onsclom'
|
||||
commit-message:
|
||||
prefix: '[framework-fixtures]'
|
||||
package-ecosystem: 'npm'
|
||||
allow:
|
||||
- dependency-name: 'parcel'
|
||||
ignore:
|
||||
- dependency-name: 'parcel'
|
||||
update-types:
|
||||
['version-update:semver-major', 'version-update:semver-patch']
|
||||
groups:
|
||||
core:
|
||||
patterns:
|
||||
- 'parcel'
|
||||
update-types:
|
||||
- 'minor'
|
||||
|
||||
- directory: /packages/static-build/test/fixtures/preact-v10
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
110
examples/nextjs/package-lock.json
generated
110
examples/nextjs/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "nextjs",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"next": "14.2.2",
|
||||
"next": "14.2.3",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
@@ -17,7 +17,7 @@
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.2",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
@@ -238,23 +238,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.2.tgz",
|
||||
"integrity": "sha512-sk72qRfM1Q90XZWYRoJKu/UWlTgihrASiYw/scb15u+tyzcze3bOuJ/UV6TBOQEeUaxOkRqGeuGUdiiuxc5oqw=="
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz",
|
||||
"integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA=="
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.2.tgz",
|
||||
"integrity": "sha512-q+Ec2648JtBpKiu/FSJm8HAsFXlNvioHeBCbTP12T1SGcHYwhqHULSfQgFkPgHDu3kzNp2Kem4J54bK4rPQ5SQ==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.3.tgz",
|
||||
"integrity": "sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob": "10.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.2.tgz",
|
||||
"integrity": "sha512-3iPgMhzbalizGwHNFUcGnDhFPSgVBHQ8aqSTAMxB5BvJG0oYrDf1WOJZlbXBgunOEj/8KMVbejEur/FpvFsgFQ==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz",
|
||||
"integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -267,9 +267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.2.tgz",
|
||||
"integrity": "sha512-x7Afi/jt0ZBRUZHTi49yyej4o8znfIMHO4RvThuoc0P+uli8Jd99y5GKjxoYunPKsXL09xBXEM1+OQy2xEL0Ag==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz",
|
||||
"integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -282,9 +282,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.2.tgz",
|
||||
"integrity": "sha512-zbfPtkk7L41ODMJwSp5VbmPozPmMMQrzAc0HAUomVeVIIwlDGs/UCqLJvLNDt4jpWgc21SjjyIn762lNGrMaUA==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz",
|
||||
"integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -297,9 +297,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.2.tgz",
|
||||
"integrity": "sha512-wPbS3pI/JU16rm3XdLvvTmlsmm1nd+sBa2ohXgBZcShX4TgOjD4R+RqHKlI1cjo/jDZKXt6OxmcU0Iys0OC/yg==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz",
|
||||
"integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -312,9 +312,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.2.tgz",
|
||||
"integrity": "sha512-NqWOHqqq8iC9tuHvZxjQ2tX+jWy2X9y8NX2mcB4sj2bIccuCxbIZrU/ThFPZZPauygajZuVQ6zediejQHwZHwQ==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz",
|
||||
"integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -327,9 +327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.2.tgz",
|
||||
"integrity": "sha512-lGepHhwb9sGhCcU7999+iK1ZZT+6rrIoVg40MP7DZski9GIZP80wORSbt5kJzh9v2x2ev2lxC6VgwMQT0PcgTA==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz",
|
||||
"integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -342,9 +342,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.2.tgz",
|
||||
"integrity": "sha512-TZSh/48SfcLEQ4rD25VVn2kdIgUWmMflRX3OiyPwGNXn3NiyPqhqei/BaqCYXViIQ+6QsG9R0C8LftMqy8JPMA==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz",
|
||||
"integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -357,9 +357,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.2.tgz",
|
||||
"integrity": "sha512-M0tBVNMEBJN2ZNQWlcekMn6pvLria7Sa2Fai5znm7CCJz4pP3lrvlSxhKdkCerk0D9E0bqx5yAo3o2Q7RrD4gA==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz",
|
||||
"integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -372,9 +372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.2.tgz",
|
||||
"integrity": "sha512-a/20E/wtTJZ3Ykv3f/8F0l7TtgQa2LWHU2oNB9bsu0VjqGuGGHmm/q6waoUNQYTVPYrrlxxaHjJcDV6aiSTt/w==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz",
|
||||
"integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1021,9 +1021,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001610",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001610.tgz",
|
||||
"integrity": "sha512-QFutAY4NgaelojVMjY63o6XlZyORPaLfyMnsl3HgnWdJUcX6K0oaJymHjH8PT5Gk7sTm8rvC/c5COUQKXqmOMA==",
|
||||
"version": "1.0.30001612",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz",
|
||||
"integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1571,12 +1571,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-next": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.2.tgz",
|
||||
"integrity": "sha512-12/uFc0KX+wUs7EDpOUGKMXBXZJiBVGdK5/m/QgXOCg2mQ0bQWoKSWNrCeOg7Vum6Kw1d1TW453W6xh+GbHquw==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.3.tgz",
|
||||
"integrity": "sha512-ZkNztm3Q7hjqvB1rRlOX8P9E/cXRL9ajRcs8jufEtwMfTVYRqnmtnaSu57QqHyBlovMuiB8LEzfLBkh5RYV6Fg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "14.2.2",
|
||||
"@next/eslint-plugin-next": "14.2.3",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
@@ -3117,11 +3117,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "14.2.2",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.2.tgz",
|
||||
"integrity": "sha512-oGwUaa2bCs47FbuxWMpOoXtBMPYpvTPgdZr3UAo+pu7Ns00z9otmYpoeV1HEiYL06AlRQQIA/ypK526KjJfaxg==",
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
|
||||
"integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==",
|
||||
"dependencies": {
|
||||
"@next/env": "14.2.2",
|
||||
"@next/env": "14.2.3",
|
||||
"@swc/helpers": "0.5.5",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
@@ -3136,15 +3136,15 @@
|
||||
"node": ">=18.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "14.2.2",
|
||||
"@next/swc-darwin-x64": "14.2.2",
|
||||
"@next/swc-linux-arm64-gnu": "14.2.2",
|
||||
"@next/swc-linux-arm64-musl": "14.2.2",
|
||||
"@next/swc-linux-x64-gnu": "14.2.2",
|
||||
"@next/swc-linux-x64-musl": "14.2.2",
|
||||
"@next/swc-win32-arm64-msvc": "14.2.2",
|
||||
"@next/swc-win32-ia32-msvc": "14.2.2",
|
||||
"@next/swc-win32-x64-msvc": "14.2.2"
|
||||
"@next/swc-darwin-arm64": "14.2.3",
|
||||
"@next/swc-darwin-x64": "14.2.3",
|
||||
"@next/swc-linux-arm64-gnu": "14.2.3",
|
||||
"@next/swc-linux-arm64-musl": "14.2.3",
|
||||
"@next/swc-linux-x64-gnu": "14.2.3",
|
||||
"@next/swc-linux-x64-musl": "14.2.3",
|
||||
"@next/swc-win32-arm64-msvc": "14.2.3",
|
||||
"@next/swc-win32-ia32-msvc": "14.2.3",
|
||||
"@next/swc-win32-x64-msvc": "14.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.2.2"
|
||||
"next": "14.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
@@ -21,6 +21,6 @@
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.2"
|
||||
"eslint-config-next": "14.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
2
examples/package.json
vendored
2
examples/package.json
vendored
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "27.4.1",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"@vercel/frameworks": "3.0.2"
|
||||
},
|
||||
"version": null
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @vercel-internals/types
|
||||
|
||||
## 1.0.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`15475c8a2`](https://github.com/vercel/vercel/commit/15475c8a2c303a1dd189ba24044fac750280dd2e), [`21f5e7375`](https://github.com/vercel/vercel/commit/21f5e7375e4cb4ceed98ab56486d09a85fa3894d)]:
|
||||
- @vercel/build-utils@8.0.0
|
||||
|
||||
## 1.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@vercel-internals/types",
|
||||
"version": "1.0.30",
|
||||
"version": "1.0.31",
|
||||
"types": "index.d.ts",
|
||||
"main": "index.d.ts",
|
||||
"files": [
|
||||
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@types/node": "14.14.31",
|
||||
"@vercel-internals/constants": "1.0.4",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"@vercel/routing-utils": "3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"source-map-support": "0.5.12",
|
||||
"ts-eager": "2.0.2",
|
||||
"ts-jest": "29.1.0",
|
||||
"turbo": "1.13.2",
|
||||
"turbo": "1.13.3",
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @vercel/build-utils
|
||||
|
||||
## 8.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- Remove legacy `avoidTopLevelInstall` logic ([#11513](https://github.com/vercel/vercel/pull/11513))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [build-utils] Add `VERCEL_PROJECT_PRODUCTION_URL` framework env var prefix ([#11506](https://github.com/vercel/vercel/pull/11506))
|
||||
|
||||
## 7.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "7.12.0",
|
||||
"version": "8.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
@@ -19,6 +19,7 @@ export function getPrefixedEnvVars({
|
||||
'VERCEL_ENV',
|
||||
'VERCEL_REGION',
|
||||
'VERCEL_BRANCH_URL',
|
||||
'VERCEL_PROJECT_PRODUCTION_URL',
|
||||
];
|
||||
const newEnvs: Envs = {};
|
||||
if (envPrefix && envs.VERCEL_URL) {
|
||||
|
||||
@@ -66,7 +66,6 @@ export interface Meta {
|
||||
filesRemoved?: string[];
|
||||
env?: Env;
|
||||
buildEnv?: Env;
|
||||
avoidTopLevelInstall?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('Test `getPrefixedEnvVars()`', () => {
|
||||
VERCEL_URL: 'example.vercel.sh',
|
||||
VERCEL_ENV: 'production',
|
||||
VERCEL_BRANCH_URL: 'example-git-main-acme.vercel.app',
|
||||
VERCEL_PROJECT_PRODUCTION_URL: 'example.com',
|
||||
USER_ENV_VAR_NOT_VERCEL: 'example.com',
|
||||
VERCEL_ARTIFACTS_TOKEN: 'abc123',
|
||||
FOO: 'bar',
|
||||
@@ -24,6 +25,7 @@ describe('Test `getPrefixedEnvVars()`', () => {
|
||||
NEXT_PUBLIC_VERCEL_URL: 'example.vercel.sh',
|
||||
NEXT_PUBLIC_VERCEL_ENV: 'production',
|
||||
NEXT_PUBLIC_VERCEL_BRANCH_URL: 'example-git-main-acme.vercel.app',
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL: 'example.com',
|
||||
TURBO_CI_VENDOR_ENV_KEY: 'NEXT_PUBLIC_VERCEL_',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
# 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
|
||||
|
||||
- Updated dependencies [[`763a6d107`](https://github.com/vercel/vercel/commit/763a6d10709ca40405634d41863c2e524917ebe1), [`15475c8a2`](https://github.com/vercel/vercel/commit/15475c8a2c303a1dd189ba24044fac750280dd2e), [`21f5e7375`](https://github.com/vercel/vercel/commit/21f5e7375e4cb4ceed98ab56486d09a85fa3894d)]:
|
||||
- @vercel/ruby@2.1.0
|
||||
- @vercel/build-utils@8.0.0
|
||||
- @vercel/node@3.0.28
|
||||
- @vercel/static-build@2.5.1
|
||||
|
||||
## 34.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`5b7960337`](https://github.com/vercel/vercel/commit/5b79603378a50fd04d5be1c3a3e5cd605b61478f)]:
|
||||
- @vercel/next@4.2.4
|
||||
|
||||
## 34.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`5758838d0`](https://github.com/vercel/vercel/commit/5758838d090b9789ca6821e1122781352021109f)]:
|
||||
- @vercel/next@4.2.3
|
||||
|
||||
## 34.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "34.1.2",
|
||||
"version": "34.1.9",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -32,17 +32,17 @@
|
||||
"node": ">= 16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"@vercel/fun": "1.1.0",
|
||||
"@vercel/go": "3.1.1",
|
||||
"@vercel/hydrogen": "1.0.2",
|
||||
"@vercel/next": "4.2.2",
|
||||
"@vercel/node": "3.0.27",
|
||||
"@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.0.5",
|
||||
"@vercel/static-build": "2.5.0",
|
||||
"@vercel/ruby": "2.1.0",
|
||||
"@vercel/static-build": "2.5.3",
|
||||
"chokidar": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -92,8 +92,8 @@
|
||||
"@types/yauzl-promise": "2.1.0",
|
||||
"@vercel-internals/constants": "1.0.4",
|
||||
"@vercel-internals/get-package-json": "1.0.0",
|
||||
"@vercel-internals/types": "1.0.30",
|
||||
"@vercel/client": "13.2.1",
|
||||
"@vercel-internals/types": "1.0.31",
|
||||
"@vercel/client": "13.2.2",
|
||||
"@vercel/error-utils": "2.0.2",
|
||||
"@vercel/frameworks": "3.0.2",
|
||||
"@vercel/fs-detectors": "5.2.3",
|
||||
|
||||
@@ -27,7 +27,7 @@ export const bisectCommand: Command = {
|
||||
description: 'Automatically open each URL in the browser',
|
||||
argument: 'URL',
|
||||
shorthand: 'o',
|
||||
type: String,
|
||||
type: Boolean,
|
||||
deprecated: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ import { URLSearchParams, parse } from 'url';
|
||||
import box from '../../util/output/box';
|
||||
import formatDate from '../../util/format-date';
|
||||
import link from '../../util/output/link';
|
||||
import getArgs from '../../util/get-args';
|
||||
import { parseArguments } from '../../util/get-args';
|
||||
import Client from '../../util/client';
|
||||
import { Deployment } from '@vercel-internals/types';
|
||||
import { normalizeURL } from '../../util/bisect/normalize-url';
|
||||
@@ -16,6 +16,8 @@ import getScope from '../../util/get-scope';
|
||||
import getDeployment from '../../util/get-deployment';
|
||||
import { help } from '../help';
|
||||
import { bisectCommand } from './command';
|
||||
import { getFlagsSpecification } from '../../util/get-flags-specification';
|
||||
import handleError from '../../util/handle-error';
|
||||
|
||||
interface Deployments {
|
||||
deployments: Deployment[];
|
||||
@@ -25,39 +27,37 @@ export default async function bisect(client: Client): Promise<number> {
|
||||
const scope = await getScope(client);
|
||||
const { contextName } = scope;
|
||||
|
||||
const argv = getArgs(client.argv.slice(2), {
|
||||
'--bad': String,
|
||||
'-b': '--bad',
|
||||
'--good': String,
|
||||
'-g': '--good',
|
||||
'--open': Boolean,
|
||||
'-o': '--open',
|
||||
'--path': String,
|
||||
'-p': '--path',
|
||||
'--run': String,
|
||||
'-r': '--run',
|
||||
});
|
||||
let parsedArgs = null;
|
||||
|
||||
if (argv['--help']) {
|
||||
const flagsSpecification = getFlagsSpecification(bisectCommand.options);
|
||||
|
||||
try {
|
||||
parsedArgs = parseArguments(client.argv.slice(2), flagsSpecification);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (parsedArgs.flags['--help']) {
|
||||
output.print(help(bisectCommand, { columns: client.stderr.columns }));
|
||||
return 2;
|
||||
}
|
||||
|
||||
let bad =
|
||||
argv['--bad'] ||
|
||||
parsedArgs.flags['--bad'] ||
|
||||
(await client.input.text({
|
||||
message: `Specify a URL where the bug occurs:`,
|
||||
validate: val => (val ? true : 'A URL must be provided'),
|
||||
}));
|
||||
let good =
|
||||
argv['--good'] ||
|
||||
parsedArgs.flags['--good'] ||
|
||||
(await client.input.text({
|
||||
message: `Specify a URL where the bug does not occur:`,
|
||||
validate: val => (val ? true : 'A URL must be provided'),
|
||||
}));
|
||||
let subpath = argv['--path'] || '';
|
||||
let run = argv['--run'] || '';
|
||||
const openEnabled = argv['--open'] || false;
|
||||
let subpath = parsedArgs.flags['--path'] || '';
|
||||
let run = parsedArgs.flags['--run'] || '';
|
||||
const openEnabled = parsedArgs.flags['--open'] || false;
|
||||
|
||||
if (run) {
|
||||
run = resolve(run);
|
||||
|
||||
@@ -10,7 +10,7 @@ export const buildCommand: Command = {
|
||||
name: 'prod',
|
||||
description: 'Build a production deployment',
|
||||
shorthand: null,
|
||||
type: String,
|
||||
type: Boolean,
|
||||
deprecated: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @vercel/client
|
||||
|
||||
## 13.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`15475c8a2`](https://github.com/vercel/vercel/commit/15475c8a2c303a1dd189ba24044fac750280dd2e), [`21f5e7375`](https://github.com/vercel/vercel/commit/21f5e7375e4cb4ceed98ab56486d09a85fa3894d)]:
|
||||
- @vercel/build-utils@8.0.0
|
||||
|
||||
## 13.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "13.2.1",
|
||||
"version": "13.2.2",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://vercel.com",
|
||||
@@ -37,7 +37,7 @@
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"@vercel/error-utils": "2.0.2",
|
||||
"@vercel/routing-utils": "3.1.0",
|
||||
"@zeit/fetch": "5.2.0",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@types/minimatch": "3.0.5",
|
||||
"@types/node": "14.18.33",
|
||||
"@types/semver": "7.3.10",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"jest-junit": "16.0.0",
|
||||
"typescript": "4.9.5"
|
||||
}
|
||||
|
||||
13
packages/functions/CHANGELOG.md
Normal file
13
packages/functions/CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# @vercel/functions
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Don't throw error if context is missing ([`0817527f9`](https://github.com/vercel/vercel/commit/0817527f9e9d0d5fceb73f21e695089349a96d3e))
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- Initial release ([#11553](https://github.com/vercel/vercel/pull/11553))
|
||||
17
packages/functions/index.d.ts
vendored
Normal file
17
packages/functions/index.d.ts
vendored
Normal 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;
|
||||
16
packages/functions/index.js
Normal file
16
packages/functions/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/* global globalThis */
|
||||
|
||||
export const waitUntil = promise => {
|
||||
if (
|
||||
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?.() ?? {};
|
||||
ctx.waitUntil?.(promise);
|
||||
};
|
||||
42
packages/functions/index.test.js
Normal file
42
packages/functions/index.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/* 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 does not throw an error when context is %s',
|
||||
input => {
|
||||
const promise = Promise.resolve();
|
||||
globalThis[Symbol.for('@vercel/request-context')] = input;
|
||||
expect(() => waitUntil(promise)).not.toThrow();
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
33
packages/functions/package.json
Normal file
33
packages/functions/package.json
Normal 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.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
# @vercel/gatsby-plugin-vercel-builder
|
||||
|
||||
## 2.0.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`15475c8a2`](https://github.com/vercel/vercel/commit/15475c8a2c303a1dd189ba24044fac750280dd2e), [`21f5e7375`](https://github.com/vercel/vercel/commit/21f5e7375e4cb4ceed98ab56486d09a85fa3894d)]:
|
||||
- @vercel/build-utils@8.0.0
|
||||
|
||||
## 2.0.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/gatsby-plugin-vercel-builder",
|
||||
"version": "2.0.25",
|
||||
"version": "2.0.26",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "0.25.24",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"@vercel/routing-utils": "3.1.0",
|
||||
"esbuild": "0.14.47",
|
||||
"etag": "1.8.1",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@types/node-fetch": "^2.3.0",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/yauzl-promise": "2.1.0",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"async-retry": "1.3.3",
|
||||
"execa": "^1.0.0",
|
||||
"fs-extra": "^7.0.0",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"devDependencies": {
|
||||
"@types/jest": "27.5.1",
|
||||
"@types/node": "14.18.33",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"execa": "3.2.0",
|
||||
"fs-extra": "11.1.0",
|
||||
"jest-junit": "16.0.0"
|
||||
|
||||
@@ -1,5 +1,49 @@
|
||||
# @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
|
||||
|
||||
- [next]: Revert .action handling for dynamic routes ([#11509](https://github.com/vercel/vercel/pull/11509))
|
||||
|
||||
## 4.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [next] Reland add .action handling for dynamic routes ([#11487](https://github.com/vercel/vercel/pull/11487))
|
||||
|
||||
## 4.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/next",
|
||||
"version": "4.2.2",
|
||||
"version": "4.2.8",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
|
||||
@@ -40,7 +40,7 @@
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/text-table": "0.2.1",
|
||||
"@types/webpack-sources": "3.2.0",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"@vercel/routing-utils": "3.1.0",
|
||||
"async-sema": "3.0.1",
|
||||
"buffer-crc32": "0.2.13",
|
||||
|
||||
@@ -201,10 +201,9 @@ export async function serverBuild({
|
||||
nextVersion,
|
||||
EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION
|
||||
);
|
||||
const hasActionOutputSupport = semver.gte(
|
||||
nextVersion,
|
||||
ACTION_OUTPUT_SUPPORT_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;
|
||||
@@ -252,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('?');
|
||||
|
||||
@@ -1589,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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1895,7 +1903,7 @@ export async function serverBuild({
|
||||
|
||||
...(appDir
|
||||
? [
|
||||
...(rscPrefetchHeader
|
||||
...(rscPrefetchHeader && experimental.ppr
|
||||
? [
|
||||
{
|
||||
src: `^${path.posix.join('/', entryDirectory, '/')}`,
|
||||
@@ -1955,19 +1963,6 @@ export async function serverBuild({
|
||||
continue: true,
|
||||
override: true,
|
||||
},
|
||||
{
|
||||
src: `^${path.posix.join('/', entryDirectory, '/')}`,
|
||||
dest: path.posix.join('/', entryDirectory, '/index.action'),
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'content-type',
|
||||
value: 'multipart/form-data;.*',
|
||||
},
|
||||
],
|
||||
continue: true,
|
||||
override: true,
|
||||
},
|
||||
{
|
||||
src: `^${path.posix.join(
|
||||
'/',
|
||||
@@ -1984,27 +1979,10 @@ export async function serverBuild({
|
||||
continue: true,
|
||||
override: true,
|
||||
},
|
||||
{
|
||||
src: `^${path.posix.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
'/((?!.+\\.action).+?)(?:/)?$'
|
||||
)}`,
|
||||
dest: path.posix.join('/', entryDirectory, '/$1.action'),
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'content-type',
|
||||
value: 'multipart/form-data;.*',
|
||||
},
|
||||
],
|
||||
continue: true,
|
||||
override: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
src: `^${path.posix.join('/', entryDirectory, '/')}`,
|
||||
src: `^${path.posix.join('/', entryDirectory, '/?')}`,
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
@@ -2067,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,
|
||||
|
||||
@@ -2465,6 +2465,7 @@ export const onPrerenderRoute =
|
||||
...(rscEnabled
|
||||
? {
|
||||
initialHeaders: {
|
||||
...initialHeaders,
|
||||
'content-type': rscContentTypeHeader,
|
||||
vary: rscVaryHeader,
|
||||
// If it contains a pre-render, then it was postponed.
|
||||
@@ -2477,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) {
|
||||
@@ -2768,7 +2785,7 @@ interface EdgeFunctionInfoV2 extends BaseEdgeFunctionInfo {
|
||||
|
||||
interface EdgeFunctionInfoV3 extends BaseEdgeFunctionInfo {
|
||||
matchers: EdgeFunctionMatcher[];
|
||||
environments: Record<string, string>;
|
||||
env: Record<string, string>;
|
||||
}
|
||||
|
||||
interface EdgeFunctionMatcher {
|
||||
@@ -3003,7 +3020,7 @@ export async function getMiddlewareBundle({
|
||||
slug: 'nextjs',
|
||||
version: nextVersion,
|
||||
},
|
||||
environment: edgeFunction.environments,
|
||||
environment: edgeFunction.env,
|
||||
});
|
||||
})(),
|
||||
routeMatchers: getRouteMatchers(edgeFunction, routesManifest),
|
||||
@@ -3183,7 +3200,7 @@ export function upgradeMiddlewareManifestV1(
|
||||
return {
|
||||
...rest,
|
||||
matchers: [{ regexp }],
|
||||
environments: {},
|
||||
env: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3211,7 +3228,7 @@ export function upgradeMiddlewareManifestV2(
|
||||
const { ...rest } = v2Info;
|
||||
return {
|
||||
...rest,
|
||||
environments: {},
|
||||
env: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
6
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/actions.js
vendored
Normal file
6
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/actions.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
'use server';
|
||||
|
||||
export async function increment(value) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return value + 1;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return children;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return children;
|
||||
}
|
||||
19
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/client/static/page.js
vendored
Normal file
19
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/client/static/page.js
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
10
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/layout.js
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/layout.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function Root({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello World</title>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
45
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/other/page.js
vendored
Normal file
45
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/other/page.js
vendored
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return children;
|
||||
}
|
||||
15
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/rsc/dynamic/page.js
vendored
Normal file
15
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/rsc/dynamic/page.js
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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' }];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return children;
|
||||
}
|
||||
15
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/rsc/static/page.js
vendored
Normal file
15
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/rsc/static/page.js
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
236
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/index.test.js
vendored
Normal file
236
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/index.test.js
vendored
Normal 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!' });
|
||||
});
|
||||
});
|
||||
});
|
||||
1
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/next.config.js
vendored
Normal file
1
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/next.config.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
9
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/package.json
vendored
Normal file
9
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/package.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "canary"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "next build && cp .next/server/server-reference-manifest.json public/"
|
||||
},
|
||||
"ignoreNextjsUpdates": true
|
||||
}
|
||||
3
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/pages/api/test.js
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/pages/api/test.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function handler(req, res) {
|
||||
res.status(200).json({ message: 'Hello from Next.js!' });
|
||||
}
|
||||
0
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/public/.keep
vendored
Normal file
0
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/public/.keep
vendored
Normal file
14
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/vercel.json
vendored
Normal file
14
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/vercel.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"src": "package.json",
|
||||
"use": "@vercel/next"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"env": {
|
||||
"NEXT_EXPERIMENTAL_STREAMING_ACTIONS": "1"
|
||||
}
|
||||
},
|
||||
"probes": []
|
||||
}
|
||||
45
packages/next/test/fixtures/00-app-dir-actions/app/other/page.js
vendored
Normal file
45
packages/next/test/fixtures/00-app-dir-actions/app/other/page.js
vendored
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -58,8 +58,8 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
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');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
});
|
||||
|
||||
it('should bypass the static cache for a server action on a page with dynamic params', async () => {
|
||||
@@ -78,8 +78,27 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
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');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
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 () => {
|
||||
@@ -98,7 +117,8 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
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-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');
|
||||
});
|
||||
});
|
||||
@@ -114,9 +134,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
});
|
||||
|
||||
it('should bypass the static cache for a server action on a page with dynamic params', async () => {
|
||||
@@ -129,9 +149,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
});
|
||||
|
||||
it('should properly invoke the action on a dynamic page', async () => {
|
||||
@@ -144,8 +164,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -163,10 +184,10 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(
|
||||
'/rsc/static/generate-static-params/[slug].action'
|
||||
'/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('MISS');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
});
|
||||
|
||||
it('should bypass the static cache for a server action when not pre-generated', async () => {
|
||||
@@ -179,10 +200,38 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(page + '.action');
|
||||
expect(res.headers.get('x-matched-path')).toBe(page);
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
// 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!' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
3
packages/next/test/fixtures/00-app-dir-actions/pages/api/test.js
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-actions/pages/api/test.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function handler(req, res) {
|
||||
res.status(200).json({ message: 'Hello from Next.js!' });
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
5
packages/next/test/fixtures/00-app-dir-no-ppr/app/nested/[...rest]/page.js
vendored
Normal file
5
packages/next/test/fixtures/00-app-dir-no-ppr/app/nested/[...rest]/page.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<p>nested app router catch-all</p>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default function Page() {
|
||||
return <p>index app page {Date.now()}</p>;
|
||||
}
|
||||
|
||||
7
packages/next/test/fixtures/00-app-dir-no-ppr/app/products/[productId]/page.jsx
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-no-ppr/app/products/[productId]/page.jsx
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
product page
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,10 @@ module.exports = {
|
||||
source: '/rewritten-to-index',
|
||||
destination: '/?fromRewrite=1',
|
||||
},
|
||||
{
|
||||
source: '/to-product/:productId.html',
|
||||
destination: '/products/:productId',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "13.5.6",
|
||||
"next": "canary",
|
||||
"react": "experimental",
|
||||
"react-dom": "experimental"
|
||||
},
|
||||
|
||||
24
packages/next/test/fixtures/00-app-dir-no-ppr/pages/nested/blog-fallback-false/[slug].js
vendored
Normal file
24
packages/next/test/fixtures/00-app-dir-no-ppr/pages/nested/blog-fallback-false/[slug].js
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/@modal/(.)cart/page.js
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/@modal/(.)cart/page.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Modal() {
|
||||
return (
|
||||
<>
|
||||
<p>cart modal!!</p>
|
||||
<Link href='/'>to index</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
3
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/@modal/default.js
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/@modal/default.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Modal() {
|
||||
return null
|
||||
}
|
||||
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/cart/page.js
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/cart/page.js
vendored
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
8
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/layout.js
vendored
Normal file
8
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/layout.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function Layout({ modal, children }) {
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -428,7 +428,7 @@ it('should handle edge functions in app with basePath', async () => {
|
||||
edgeFunctions.add(item);
|
||||
}
|
||||
}
|
||||
expect(lambdas.size).toBe(2);
|
||||
expect(lambdas.size).toBe(1);
|
||||
expect(edgeFunctions.size).toBe(4);
|
||||
});
|
||||
|
||||
@@ -452,5 +452,5 @@ it('should not generate lambdas that conflict with static index route in app wit
|
||||
lambdas.add(item);
|
||||
}
|
||||
}
|
||||
expect(lambdas.size).toBe(3);
|
||||
expect(lambdas.size).toBe(1);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @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
|
||||
|
||||
- Updated dependencies [[`15475c8a2`](https://github.com/vercel/vercel/commit/15475c8a2c303a1dd189ba24044fac750280dd2e), [`21f5e7375`](https://github.com/vercel/vercel/commit/21f5e7375e4cb4ceed98ab56486d09a85fa3894d)]:
|
||||
- @vercel/build-utils@8.0.0
|
||||
|
||||
## 3.0.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/node",
|
||||
"version": "3.0.27",
|
||||
"version": "3.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
|
||||
@@ -23,8 +23,8 @@
|
||||
"@edge-runtime/node-utils": "2.3.0",
|
||||
"@edge-runtime/primitives": "4.1.0",
|
||||
"@edge-runtime/vm": "3.2.0",
|
||||
"@types/node": "14.18.33",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@types/node": "16.18.11",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"@vercel/error-utils": "2.0.2",
|
||||
"@vercel/nft": "0.26.4",
|
||||
"@vercel/static-config": "3.0.0",
|
||||
@@ -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",
|
||||
|
||||
30
packages/node/src/awaiter.ts
Normal file
30
packages/node/src/awaiter.ts
Normal 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)
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 '^/.*$';
|
||||
|
||||
12
packages/node/test/dev-fixtures/wait-until-ctx-edge.js
Normal file
12
packages/node/test/dev-fixtures/wait-until-ctx-edge.js
Normal 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' };
|
||||
@@ -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) });
|
||||
}
|
||||
10
packages/node/test/dev-fixtures/wait-until-ctx-node.js
Normal file
10
packages/node/test/dev-fixtures/wait-until-ctx-node.js
Normal 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) });
|
||||
}
|
||||
13
packages/node/test/dev-fixtures/wait-until-edge.js
Normal file
13
packages/node/test/dev-fixtures/wait-until-edge.js
Normal 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' };
|
||||
11
packages/node/test/dev-fixtures/wait-until-node.js
Normal file
11
packages/node/test/dev-fixtures/wait-until-node.js
Normal 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');
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "14.18.33",
|
||||
"@types/which": "3.0.0",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"execa": "^1.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"jest-junit": "16.0.0",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@types/aws-lambda": "8.10.19",
|
||||
"@types/node": "14.18.33",
|
||||
"@types/semver": "6.0.0",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"execa": "3.2.0",
|
||||
"fs-extra": "11.1.0",
|
||||
"jest-junit": "16.0.0"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@types/jest": "27.5.1",
|
||||
"@types/node": "14.18.33",
|
||||
"@types/semver": "7.3.13",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"jest-junit": "16.0.0",
|
||||
"path-to-regexp": "6.2.1",
|
||||
"semver": "7.5.2"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @vercel/ruby
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add support for Ruby 3.3 ([#11497](https://github.com/vercel/vercel/pull/11497))
|
||||
|
||||
- Remove legacy `avoidTopLevelInstall` logic ([#11513](https://github.com/vercel/vercel/pull/11513))
|
||||
|
||||
## 2.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@vercel/ruby",
|
||||
"author": "Nathan Cahill <nathan@nathancahill.com>",
|
||||
"version": "2.0.5",
|
||||
"version": "2.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",
|
||||
@@ -23,10 +23,12 @@
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "8.0.0",
|
||||
"@types/semver": "6.0.0",
|
||||
"@vercel/build-utils": "7.12.0",
|
||||
"@types/which": "3.0.0",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"execa": "2.0.4",
|
||||
"fs-extra": "^7.0.1",
|
||||
"jest-junit": "16.0.0",
|
||||
"semver": "6.3.1"
|
||||
"semver": "6.3.1",
|
||||
"which": "3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EOL } from 'os';
|
||||
import { join, dirname, relative } from 'path';
|
||||
import { join, dirname } from 'path';
|
||||
import execa from 'execa';
|
||||
import {
|
||||
ensureDir,
|
||||
@@ -49,7 +49,7 @@ async function bundleInstall(
|
||||
bundleDir: string,
|
||||
gemfilePath: string,
|
||||
rubyPath: string,
|
||||
runtime: string
|
||||
major: number
|
||||
) {
|
||||
debug(`running "bundle install --deployment"...`);
|
||||
const bundleAppConfig = await getWriteableDirectory();
|
||||
@@ -73,6 +73,15 @@ async function bundleInstall(
|
||||
gemfilePath,
|
||||
gemfileContent.replace('ruby "~> 3.2.x"', 'ruby "~> 3.2.0"')
|
||||
);
|
||||
} else if (gemfileContent.includes('ruby "~> 3.3.x"')) {
|
||||
// Gemfile contains "3.3.x" which will cause an error message:
|
||||
// "Your Ruby patchlevel is 0, but your Gemfile specified -1"
|
||||
// See https://github.com/rubygems/bundler/blob/3f0638c6c8d340c2f2405ecb84eb3b39c433e36e/lib/bundler/errors.rb#L49
|
||||
// We must correct to the actual version in the build container.
|
||||
await writeFile(
|
||||
gemfilePath,
|
||||
gemfileContent.replace('ruby "~> 3.3.x"', 'ruby "~> 3.3.0"')
|
||||
);
|
||||
}
|
||||
|
||||
const bundlerEnv = cloneEnv(process.env, {
|
||||
@@ -83,9 +92,9 @@ async function bundleInstall(
|
||||
BUNDLE_JOBS: '4',
|
||||
});
|
||||
|
||||
// Lambda "ruby3.2" runtime does not include "webrick",
|
||||
// which is needed for the `vc_init.rb` entrypoint file
|
||||
if (runtime === 'ruby3.2') {
|
||||
// "webrick" needs to be installed for Ruby 3+ to fix runtime error:
|
||||
// webrick is not part of the default gems since Ruby 3.0.0. Install webrick from RubyGems.
|
||||
if (major >= 3) {
|
||||
const result = await execa('bundler', ['add', 'webrick'], {
|
||||
cwd: dirname(gemfilePath),
|
||||
stdio: 'pipe',
|
||||
@@ -143,7 +152,7 @@ export const build: BuildV3 = async ({
|
||||
const gemfileContents = gemfilePath
|
||||
? await readFile(gemfilePath, 'utf8')
|
||||
: '';
|
||||
const { gemHome, bundlerPath, vendorPath, runtime, rubyPath } =
|
||||
const { gemHome, bundlerPath, vendorPath, runtime, rubyPath, major } =
|
||||
await installBundler(meta, gemfileContents);
|
||||
process.env.GEM_HOME = gemHome;
|
||||
debug(`Checking existing vendor directory at "${vendorPath}"`);
|
||||
@@ -178,24 +187,9 @@ export const build: BuildV3 = async ({
|
||||
'did not find a vendor directory but found a Gemfile, bundling gems...'
|
||||
);
|
||||
|
||||
const fileAtRoot = relative(workPath, gemfilePath) === gemfileName;
|
||||
|
||||
// If the `Gemfile` is located in the Root Directory of the project and
|
||||
// the new File System API is used (`avoidTopLevelInstall`), the Install Command
|
||||
// will have already installed its dependencies, so we don't need to do it again.
|
||||
if (meta.avoidTopLevelInstall && fileAtRoot) {
|
||||
debug('Skipping `bundle install` — already handled by Install Command');
|
||||
} else {
|
||||
// try installing. this won't work if native extensions are required.
|
||||
// if that's the case, gems should be vendored locally before deploying.
|
||||
await bundleInstall(
|
||||
bundlerPath,
|
||||
bundleDir,
|
||||
gemfilePath,
|
||||
rubyPath,
|
||||
runtime
|
||||
);
|
||||
}
|
||||
// try installing. this won't work if native extensions are required.
|
||||
// if that's the case, gems should be vendored locally before deploying.
|
||||
await bundleInstall(bundlerPath, bundleDir, gemfilePath, rubyPath, major);
|
||||
}
|
||||
} else {
|
||||
debug('found vendor directory, skipping "bundle install"...');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user