Compare commits

...

76 Commits

Author SHA1 Message Date
Leo Lamprecht
d3ef240f6e Publish Canary
- @vercel/build-utils@2.12.3-canary.42
 - vercel@23.1.3-canary.67
 - @vercel/client@10.2.3-canary.45
 - vercel-plugin-middleware@0.0.0-canary.19
 - vercel-plugin-go@1.0.0-canary.30
 - vercel-plugin-node@1.12.2-canary.34
 - vercel-plugin-python@1.0.0-canary.31
 - vercel-plugin-ruby@1.0.0-canary.30
 - @vercel/python@2.1.2-canary.2
2021-12-08 15:53:14 +01:00
Leo Lamprecht
5b26ebc7b8 Make Python CLI Plugin work (#7155) 2021-12-08 15:52:43 +01:00
Leo Lamprecht
3427ad6ce0 Publish Canary
- vercel@23.1.3-canary.66
2021-12-08 12:50:58 +01:00
Leo Lamprecht
4ab5e4326b Improved Vercel CLI link (#7151) 2021-12-08 12:50:27 +01:00
Leo Lamprecht
d24a3ce3ab Publish Canary
- @vercel/client@10.2.3-canary.44
2021-12-08 12:10:44 +01:00
Steven
29a44db8d9 [client] Fix duplicate files when analyzing nft.json (#7150)
This PR fixes a regression from #7144 where a duplicate file was added if `nft.json` referenced an existing file.

I also refactored the prepared files logic to avoid cloning the array in every loop iteration.

Review [without whitespace](https://github.com/vercel/vercel/pull/7150/files?diff=split&w=1) will make it easier to understand.

- Related to https://github.com/vercel/runtimes/issues/304
2021-12-08 01:40:49 +00:00
Steven
695f3a9212 Publish Canary
- vercel@23.1.3-canary.65
 - @vercel/client@10.2.3-canary.43
 - vercel-plugin-middleware@0.0.0-canary.18
2021-12-07 18:25:02 -05:00
Steven
3ff777b8ed [client] Resolve .nft.json files when vc deploy --prebuilt (#7144)
This ensures that using `vc deploy --prebuilt` will also upload any files that `.output/**/.nft.json` points to and also handle the Root Directory correctly since `vc build` emits `rootdir/.output`.

- Related to https://github.com/vercel/runtimes/issues/304.
2021-12-07 18:17:58 -05:00
Tommaso De Rossi
d94b9806ab [middleware] Define env vars when building _middleware.js with esbuild (#7087)
### Related Issues

> Fixes #7086
> Related to #7086

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2021-12-07 22:16:46 +00:00
Leo Lamprecht
35c8fc2729 Publish Canary
- @vercel/build-utils@2.12.3-canary.41
 - vercel@23.1.3-canary.64
 - @vercel/client@10.2.3-canary.42
 - vercel-plugin-middleware@0.0.0-canary.17
 - vercel-plugin-go@1.0.0-canary.29
 - vercel-plugin-node@1.12.2-canary.33
 - vercel-plugin-python@1.0.0-canary.30
 - vercel-plugin-ruby@1.0.0-canary.29
2021-12-07 21:13:41 +01:00
Leo Lamprecht
0a468fd6d7 Correctly clean up files for CLI Plugins (#7149)
* Correctly clean up files for CLI Plugins

* Cleaned up the code
2021-12-07 21:13:29 +01:00
Leo Lamprecht
d31ebbabe4 Publish Canary
- @vercel/build-utils@2.12.3-canary.40
 - vercel@23.1.3-canary.63
 - @vercel/client@10.2.3-canary.41
 - vercel-plugin-middleware@0.0.0-canary.16
 - vercel-plugin-go@1.0.0-canary.28
 - vercel-plugin-node@1.12.2-canary.32
 - vercel-plugin-python@1.0.0-canary.29
 - vercel-plugin-ruby@1.0.0-canary.28
2021-12-07 17:46:08 +01:00
Leo Lamprecht
09c9b71adb Adjust import statements inside Runtime launchers (#7148)
* Added basic logic

* Polished basic logic

* Made logic actually replace content

* Perfected the logic

* Added comment

* Simplified logic

* Added another comment

* Added debug log

* More detailed debug log

* Update packages/build-utils/src/convert-runtime-to-plugin.ts

Co-authored-by: Andy <AndyBitz@users.noreply.github.com>

* Update packages/build-utils/src/convert-runtime-to-plugin.ts

Co-authored-by: Steven <steven@ceriously.com>

* Simpler logic

Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
Co-authored-by: Steven <steven@ceriously.com>
2021-12-07 17:44:59 +01:00
Leo Lamprecht
5975db4d66 Fixed middleware tests (#7146) 2021-12-07 01:10:58 +01:00
Leo Lamprecht
2c86ac654c Publish Canary
- @vercel/build-utils@2.12.3-canary.39
 - vercel@23.1.3-canary.62
 - @vercel/client@10.2.3-canary.40
 - vercel-plugin-middleware@0.0.0-canary.15
 - vercel-plugin-go@1.0.0-canary.27
 - vercel-plugin-node@1.12.2-canary.31
 - vercel-plugin-python@1.0.0-canary.28
 - vercel-plugin-ruby@1.0.0-canary.27
2021-12-06 23:37:05 +01:00
Leo Lamprecht
ca5f066eb9 Simplify NFT output logic for CLI and CLI Plugins (#7143)
* Simplify NFT output logic for CLI Plugins

* Made tests pass

* Remove useless logic from Vercel CLI

* Update packages/build-utils/src/convert-runtime-to-plugin.ts

Co-authored-by: Steven <steven@ceriously.com>

* Simplified CLI code

* Removed useless file

Co-authored-by: Steven <steven@ceriously.com>
2021-12-06 23:35:07 +01:00
Leo Lamprecht
410ef86102 Support nested API Routes and fix handler for CLI Plugins (#7141)
We have identified that the `handler` for Lambdas does not support a dot-preceded path for Ruby, Python, and probably other languages, so we're adjusting the File System API to change `.output` inside the Lambda to something else, which requires version `2` of `functions-manifest.json`.

Furthermore, we're also bumping the `.nft.json` files to version `2`, which allows `output` to be relative to the NFT file itself, so that, inside the Lambda, the behavior mentioned at the top can be applied by the File System API.

As a nice side effect, this will also support nested API Routes, because it'll place all the dependencies next to every API Route, meaning that the launcher will have access to all of them (bundling multiple API Routes or Pages into the same Lambda currently doesn't work for non-Next.js anyways, because of https://github.com/vercel/runtimes/issues/305).

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2021-12-06 20:53:07 +00:00
Steven
6792edf32a Publish Canary
- @vercel/build-utils@2.12.3-canary.38
 - vercel@23.1.3-canary.61
 - @vercel/client@10.2.3-canary.39
 - @vercel/frameworks@0.5.1-canary.17
 - vercel-plugin-middleware@0.0.0-canary.14
 - vercel-plugin-go@1.0.0-canary.26
 - vercel-plugin-node@1.12.2-canary.30
 - vercel-plugin-python@1.0.0-canary.27
 - vercel-plugin-ruby@1.0.0-canary.26
2021-12-06 14:36:06 -05:00
Steven
67de167a7e [frameworks][cli] Remove duplicate getFsOutputDir() definitions (#7124) 2021-12-06 14:34:30 -05:00
Leo Lamprecht
0c5c05d90b Prevent CLI Plugins from overwriting files (#7140)
* Prevent CLI Plugins from overwriting files

* Revert "Temporarily remove CLI Plugin linking (#7138)"

This reverts commit d6a5aa4f6d.
2021-12-06 17:22:55 +01:00
Leo Lamprecht
fe43c9c4b2 Publish Canary
- @vercel/build-utils@2.12.3-canary.37
 - vercel@23.1.3-canary.60
 - @vercel/client@10.2.3-canary.38
 - vercel-plugin-middleware@0.0.0-canary.13
 - vercel-plugin-go@1.0.0-canary.25
 - vercel-plugin-node@1.12.2-canary.29
 - vercel-plugin-python@1.0.0-canary.26
 - vercel-plugin-ruby@1.0.0-canary.25
2021-12-06 14:28:12 +01:00
Leo Lamprecht
d6a5aa4f6d Temporarily remove CLI Plugin linking (#7138) 2021-12-06 14:25:22 +01:00
Leo Lamprecht
1c3701628d Publish Canary
- @vercel/build-utils@2.12.3-canary.36
 - vercel@23.1.3-canary.59
 - @vercel/client@10.2.3-canary.37
 - vercel-plugin-middleware@0.0.0-canary.12
 - vercel-plugin-go@1.0.0-canary.24
 - vercel-plugin-node@1.12.2-canary.28
 - vercel-plugin-python@1.0.0-canary.25
 - vercel-plugin-ruby@1.0.0-canary.24
2021-12-04 17:01:20 +01:00
Leo Lamprecht
45689f22ab Correctly position dependencies for CLI Plugins (#7133)
Previously, CLI Plugins would try to mount the user-provided request handler (e.g. `api/test.rb`) at the same position inside the Lambda at which the launcher was located (e.g. `api/test.rb`), which would cause the launcher to be overwritten.

With this change, all the destination mounting points for NFT input files are becoming relative to the `.output/server/pages/api` directory instead of `.output/server/pages`, so they should no longer overwrite the launcher, and instead be loaded from the launcher, like normal.

This PR might have two problems:

- If imports in Ruby/Python/etc are relative to `cwd` and not the file from which the import is executed (which I doubt), this would fail.
- We might have to replace `api` with the exact sub folder of the API Route within `.output/server/pages/api` if the current change doesn't yet work for nested paths. Although that also means repeating all of the other dependencies (not just the user-provided request handler) in a different location for every single API Route.

The two above will be tested after this PR was merged, as there currently isn't a way to test `vercel-plugin-go`, `vercel-plugin-python`, and `vercel-plugin-ruby` without publishing a canary, because they don't bundle `@vercel/build-utils`, which was the package that was just updated.

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2021-12-04 16:00:15 +00:00
Leo Lamprecht
2c3ddffaac Publish Canary
- @vercel/build-utils@2.12.3-canary.35
 - vercel@23.1.3-canary.58
 - @vercel/client@10.2.3-canary.36
 - vercel-plugin-middleware@0.0.0-canary.11
 - vercel-plugin-go@1.0.0-canary.23
 - vercel-plugin-node@1.12.2-canary.27
 - vercel-plugin-python@1.0.0-canary.24
 - vercel-plugin-ruby@1.0.0-canary.23
 - @vercel/ruby@1.2.10-canary.0
2021-12-04 01:30:37 +01:00
Leo Lamprecht
c3ea0195c2 Fixed Lambda handler for compiled languages (#7129)
* Use correct Lambda handler for compiled languages

* Use the correct entrypoint

* Fixed the logic

* Perfected it

* Simpler code

* Update packages/build-utils/src/convert-runtime-to-plugin.ts

Co-authored-by: Andy <AndyBitz@users.noreply.github.com>

* Handle edge cases and simplify logic

* Fixed the logic yet again to work perfectly

* Normalize Page path

* Simplified everything

* Fixed the tests

Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
2021-12-04 01:29:46 +01:00
Gary Borton
5f5e50cff0 [middleware] Make edge functions be in "strict mode" (#7118)
Co-authored-by: Leo Lamprecht <leo@vercel.com>
Co-authored-by: Steven <steven@ceriously.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2021-12-03 15:22:21 -08:00
Steven
160f4d46d9 Publish Stable
- @vercel/ruby@1.2.9
2021-12-03 16:39:00 -05:00
Steven
8d619bd7cc Publish Canary
- vercel@23.1.3-canary.57
 - vercel-plugin-ruby@1.0.0-canary.22
 - @vercel/ruby@1.2.8-canary.7
2021-12-03 15:43:53 -05:00
Steven
b94337d842 [ruby] Show error when Ruby 2.5.x detected (#7126)
* [ruby] Show error when Ruby 2.5.x detected

* Add test with ruby 2.5.x
2021-12-03 14:34:01 -05:00
Leo Lamprecht
34f4222ca2 Publish Canary
- @vercel/build-utils@2.12.3-canary.34
 - vercel@23.1.3-canary.56
 - @vercel/client@10.2.3-canary.35
 - vercel-plugin-middleware@0.0.0-canary.10
 - vercel-plugin-go@1.0.0-canary.22
 - vercel-plugin-node@1.12.2-canary.26
 - vercel-plugin-python@1.0.0-canary.23
 - vercel-plugin-ruby@1.0.0-canary.21
2021-12-03 20:15:25 +01:00
Leo Lamprecht
5de045edd7 Use correct Lambda handler for CLI Plugins (#7128)
* Use correct Lambda handler for CLI Plugins

* Tweaked comment

* Fixed tests
2021-12-03 20:15:05 +01:00
Leo Lamprecht
5efd3b98de Publish Canary
- @vercel/build-utils@2.12.3-canary.33
 - vercel@23.1.3-canary.55
 - @vercel/client@10.2.3-canary.34
 - vercel-plugin-middleware@0.0.0-canary.9
 - vercel-plugin-go@1.0.0-canary.21
 - vercel-plugin-node@1.12.2-canary.25
 - vercel-plugin-python@1.0.0-canary.22
 - vercel-plugin-ruby@1.0.0-canary.20
2021-12-03 19:10:29 +01:00
Leo Lamprecht
82c83312c7 Fixed Legacy Runtime output generation (#7127)
* Corrected CLI Plugin output creation

* Wait for the linking to be done before removing anything

* Make it work

* Renamed variables

* Made everything work

* Removed debugging

* Fixed comment typos
2021-12-03 19:09:59 +01:00
Andy Bitz
5ccb983007 Publish Canary
- vercel@23.1.3-canary.54
 - vercel-plugin-middleware@0.0.0-canary.8
2021-12-03 12:12:03 +01:00
Steven
7a921399be [middleware] Fix dependencies (#7125) 2021-12-02 20:38:07 -05:00
Andy
3900f2f982 [cli] Support for next export with vercel build (#7122)
* [cli] Support `next export` in `vercel build`

* Add debug line

* Remove unused import

* Ensure file is copied

* Return in `getNextExportStatus`

* Include dotNextDir

Co-authored-by: Steven <steven@ceriously.com>
2021-12-02 20:31:17 -05:00
Steven
09939f1e07 Update github codeowners (#7123)
* Add gary and javi

* Remove timothy and coetry

* Remove rdev

* Add jaredpalmer

Co-authored-by: Nathan Rajlich <n@n8.io>
2021-12-02 19:20:18 -05:00
Leo Lamprecht
fc3a3ca81f Use Functions Manifest for Middleware (#7119)
### Related Issues

This fixes https://github.com/vercel/runtimes/issues/299.

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2021-12-03 00:12:01 +00:00
Leo Lamprecht
ba7bf2e4a6 Publish Canary
- @vercel/build-utils@2.12.3-canary.32
 - vercel@23.1.3-canary.53
 - @vercel/client@10.2.3-canary.33
 - vercel-plugin-go@1.0.0-canary.20
 - vercel-plugin-node@1.12.2-canary.24
 - vercel-plugin-python@1.0.0-canary.21
 - vercel-plugin-ruby@1.0.0-canary.19
2021-12-02 23:51:51 +01:00
Leo Lamprecht
00641037fc Corrected input paths for CLI Plugins (#7121)
* Use correct paths for outputs

* Fixed tests

This reverts commit 7c4baeaafaf41609f47c97a09f5e9647fd8b89ee.

* Revert "Fixed tests"

This reverts commit 59c10d18c63f8404c3b0c361c3769b62524316f1.

* Revert "Use correct paths for outputs"

This reverts commit 23a0b34fad1e4932755a39975ae1dfa07acb2dd9.

* Corrected input paths for CLI Plugins

* Fixed tests

This reverts commit 7c4baeaafaf41609f47c97a09f5e9647fd8b89ee.

* Revert "Fixed tests"

This reverts commit 9612d2a9eb19240a5a4489406ada17a6a5bb3806.

* Fixed tests

* Delete vc__handler__python.py
2021-12-02 23:51:39 +01:00
Leo Lamprecht
6f4a1b527b Publish Canary
- @vercel/build-utils@2.12.3-canary.31
 - vercel@23.1.3-canary.52
 - @vercel/client@10.2.3-canary.32
 - vercel-plugin-go@1.0.0-canary.19
 - vercel-plugin-node@1.12.2-canary.23
 - vercel-plugin-python@1.0.0-canary.20
 - vercel-plugin-ruby@1.0.0-canary.18
2021-12-02 22:41:17 +01:00
Leo Lamprecht
1b95576dd2 Fixed Go, Ruby, and Python CLI Plugin output generation (#7117)
* Fixed error with API directory

* Made output work

* Use handler as API Route

* Correctly find the handler

* Fixed a missing instance

* Made handler logic work

* Made it work as expected

* Exclude unnecessary files

* Use a method that always works

* Additional comment

* Made everything work

* Cleaner tests

* Clean up all the useless files

* Fixed missing instance

* Speed up the code

* Removed useless lines

* Update packages/build-utils/src/convert-runtime-to-plugin.ts

Co-authored-by: Andy <AndyBitz@users.noreply.github.com>

* Clarified comment

* Use relative logic again

* Fixed tests

* Deleted useless file

Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
2021-12-02 22:18:43 +01:00
Leo Lamprecht
9227471aca Publish Canary
- @vercel/build-utils@2.12.3-canary.30
 - vercel@23.1.3-canary.51
 - @vercel/client@10.2.3-canary.31
 - vercel-plugin-go@1.0.0-canary.18
 - vercel-plugin-node@1.12.2-canary.22
 - vercel-plugin-python@1.0.0-canary.19
 - vercel-plugin-ruby@1.0.0-canary.17
2021-12-02 15:24:34 +01:00
Leo Lamprecht
bf060296eb Fixed typo (#7115) 2021-12-02 15:19:32 +01:00
Leo Lamprecht
9b3aa41f2e Publish Canary
- @vercel/build-utils@2.12.3-canary.29
 - vercel@23.1.3-canary.50
 - @vercel/client@10.2.3-canary.30
 - vercel-plugin-go@1.0.0-canary.17
 - vercel-plugin-node@1.12.2-canary.21
 - vercel-plugin-python@1.0.0-canary.18
 - vercel-plugin-ruby@1.0.0-canary.16
2021-12-02 15:02:00 +01:00
Leo Lamprecht
ae36585cdb Filter CLI Plugin output (#7114)
* Filter CLI Plugin output

* Only apply ignorefile check in the beginning
2021-12-02 15:01:17 +01:00
Leo Lamprecht
e4c636ddd2 Publish Canary
- vercel-plugin-go@1.0.0-canary.16
 - vercel-plugin-python@1.0.0-canary.17
 - vercel-plugin-ruby@1.0.0-canary.15
2021-12-02 14:14:14 +01:00
Leo Lamprecht
ae3b25be4b Made CLI Plugin publishing work (#7113) 2021-12-02 14:10:23 +01:00
Andy Bitz
a64ed13a40 Publish Canary
- vercel@23.1.3-canary.49
2021-12-02 12:55:04 +01:00
Andy
6c1c0e6676 [cli] Ignore required-server-files.json if it does not exist (#7111) 2021-12-02 12:54:30 +01:00
Leo Lamprecht
82fdd5d121 Publish Canary
- vercel@23.1.3-canary.48
 - vercel-plugin-go@1.0.0-canary.15
 - vercel-plugin-node@1.12.2-canary.20
 - vercel-plugin-python@1.0.0-canary.16
 - vercel-plugin-ruby@1.0.0-canary.14
 - @vercel/static-config@0.0.1-canary.1
2021-12-02 11:46:28 +01:00
Nathan Rajlich
8b40f4435e [api] Use the new File System API (#7108)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-12-02 11:45:57 +01:00
Leo Lamprecht
38c87602bb Renamed runtime to use in static JS config (#7106)
### Related Issues

This applies what was mentioned in https://github.com/vercel/runtimes/issues/288#issuecomment-984101750.

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2021-12-02 00:37:09 +00:00
Nathan Rajlich
7aef3013e7 [cli] Use "127.0.0.1" instead of "localhost" in vc dev (#7094)
Node.js doesn't like when a hostname resolves to an IPv6 address (https://stackoverflow.com/a/15244890/376773) so use the IPv4 localhost IP address instead. Specifically this fixes vc dev on Node.js 17 which now prefers IPv6 by default.

Slack thread: https://vercel.slack.com/archives/C01A2M9R8RZ/p1638330248263400
2021-12-01 23:29:26 +00:00
Nathan Rajlich
c18676ab4d Publish Canary
- vercel-plugin-go@1.0.0-canary.14
 - vercel-plugin-python@1.0.0-canary.15
 - vercel-plugin-ruby@1.0.0-canary.13
2021-12-01 14:28:40 -08:00
Leo Lamprecht
df450c815d Properly publish CLI Plugins (#7103)
After https://github.com/vercel/vercel/pull/7088, `dist` now contains `package.json`, which made `tsc` move `index.js` a level deeper, effectively breaking the `main` property of all of the affected CLI Plugins.

This change makes them work again. 

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2021-12-01 20:59:46 +00:00
Andy Bitz
792ab38760 Publish Canary
- @vercel/build-utils@2.12.3-canary.28
 - vercel@23.1.3-canary.47
 - @vercel/client@10.2.3-canary.29
 - vercel-plugin-go@1.0.0-canary.13
 - vercel-plugin-node@1.12.2-canary.19
 - vercel-plugin-python@1.0.0-canary.14
 - vercel-plugin-ruby@1.0.0-canary.12
 - @vercel/ruby@1.2.8-canary.6
2021-12-01 15:32:05 +01:00
Leo Lamprecht
0bba3e76c1 Corrected dependency installation systems (#7088)
* Avoid unnecessary Gemfile installations

* Do not bundle `.output` or `.vercel` into Lambdas

* Use the same input hash format for all CLI Plugins and `vercel build`

* Fixed unit tests

* Fixed the unit tests again

* Fixed the unit tests

* Fixed all the tests

* Exclude useless files

* Consider `.vercelignore` and `.nowignore`

* Fixed error

* Reverted changes

* Deleted useless file

* Fixed tests

* Share input hash format with `vercel-plugin-node`

* Make output inspectable

* Fixed build error

* Extended comment

* Bump Ruby version

* Update Gemfiles

* Update bundles

* Fixed tests

Co-authored-by: Andy Bitz <artzbitz@gmail.com>
2021-12-01 15:31:27 +01:00
Andy Bitz
3d961ffbb9 Publish Canary
- vercel@23.1.3-canary.46
2021-11-30 19:25:13 +01:00
Andy
a3039f57bb [cli] Fix vc build with nested app dir (#7083)
* [cli] Fix `vc build` with nested app dir

* Set NEXT_PRIVATE_OUTPUT_TRACE_ROOT
2021-11-30 18:30:51 +01:00
Andy Bitz
5499fa9a04 Publish Canary
- @vercel/build-utils@2.12.3-canary.27
 - vercel@23.1.3-canary.45
 - @vercel/client@10.2.3-canary.28
 - vercel-plugin-go@1.0.0-canary.12
 - vercel-plugin-node@1.12.2-canary.18
 - vercel-plugin-python@1.0.0-canary.13
 - vercel-plugin-ruby@1.0.0-canary.11
2021-11-30 16:17:47 +01:00
Leo Lamprecht
b9fd64faff Stop passing functions and regions to Runtimes (#7085)
* Stop passing `functions` and `regions` to Runtimes

* Added required types back

* Removed `vercelConfig` from `vercel-plugin-node`

* Fixed unit test

* Fixed test
2021-11-30 16:16:53 +01:00
Andy Bitz
1202ff7b2b Publish Canary
- @vercel/build-utils@2.12.3-canary.26
 - vercel@23.1.3-canary.44
 - @vercel/client@10.2.3-canary.27
 - @vercel/frameworks@0.5.1-canary.16
 - vercel-plugin-go@1.0.0-canary.11
 - vercel-plugin-node@1.12.2-canary.17
 - vercel-plugin-python@1.0.0-canary.12
 - vercel-plugin-ruby@1.0.0-canary.10
 - @vercel/python@2.1.2-canary.1
 - @vercel/ruby@1.2.8-canary.5
2021-11-30 14:37:53 +01:00
Leo Lamprecht
abd9f019f1 Don't install Ruby or Python dependencies unnecessarily (#7084)
* Don't install Ruby or Python dependencies unnecessarily

* Less repeated code

* Cleaner code
2021-11-30 14:36:27 +01:00
Leo Lamprecht
edb5eead81 Speed up Remix Template (#7077)
* Replaced Remix Template

* Added npm lockfile for Remix Template

* Added npm lockfile for Next.js Template

* Added Remix logo
2021-11-29 16:01:23 +01:00
Andy Bitz
6b865ff753 Publish Canary
- @vercel/build-utils@2.12.3-canary.25
 - vercel@23.1.3-canary.43
 - @vercel/client@10.2.3-canary.26
 - @vercel/frameworks@0.5.1-canary.15
 - vercel-plugin-go@1.0.0-canary.10
 - vercel-plugin-node@1.12.2-canary.16
 - vercel-plugin-python@1.0.0-canary.11
 - vercel-plugin-ruby@1.0.0-canary.9
2021-11-29 14:26:12 +01:00
Andy
4fd0734c48 [cli] Consider envPrefix and outputDirectory for vercel build (#7069)
* [cli] Consider `envPrefix` for the framework

* Fix env

* Remove type

* Resolve .nft.json files correctly

* Fix public and static directory handling

* Do not use .replace

* Consider the output directory
2021-11-29 14:23:10 +01:00
Leo Lamprecht
f815421acb Renamed the Remix logo file (#7074) 2021-11-29 12:14:09 +01:00
Logan McAnsh
5da926fee1 chore: update remix logo (#7070) 2021-11-29 11:37:46 +01:00
Andy Bitz
3559531e4c Publish Canary
- @vercel/build-utils@2.12.3-canary.24
 - vercel@23.1.3-canary.42
 - @vercel/client@10.2.3-canary.25
 - @vercel/frameworks@0.5.1-canary.14
 - vercel-plugin-go@1.0.0-canary.9
 - vercel-plugin-node@1.12.2-canary.15
 - vercel-plugin-python@1.0.0-canary.10
 - vercel-plugin-ruby@1.0.0-canary.8
2021-11-25 12:01:09 +01:00
Leo Lamprecht
449a3b3648 Updated Remix template and ensured correct headers (#7064)
* Updated Remix Example

* Adjusted config as necessary

* Updated gitignore

* Fixed default Remix headers

* Removed useless files

* Fixed README

* Added newline
2021-11-25 11:57:57 +01:00
Steven
7bd338618c Publish Canary
- @vercel/build-utils@2.12.3-canary.23
 - vercel@23.1.3-canary.41
 - @vercel/client@10.2.3-canary.24
 - vercel-plugin-go@1.0.0-canary.8
 - vercel-plugin-node@1.12.2-canary.14
 - vercel-plugin-python@1.0.0-canary.9
 - vercel-plugin-ruby@1.0.0-canary.7
2021-11-24 22:03:54 -05:00
Steven
9048a6f584 [build-utils] Fix zero config routes for vercel build (#7063) 2021-11-24 22:00:49 -05:00
Steven
0cacb1bdac Publish Canary
- @vercel/build-utils@2.12.3-canary.22
 - vercel@23.1.3-canary.40
 - @vercel/client@10.2.3-canary.23
 - vercel-plugin-go@1.0.0-canary.7
 - vercel-plugin-node@1.12.2-canary.13
 - vercel-plugin-python@1.0.0-canary.8
 - vercel-plugin-ruby@1.0.0-canary.6
2021-11-24 18:12:26 -05:00
Steven
318bf35f82 [build-utils] Add support for writing routes-manifest.json (#7062)
* [build-utils] Add support for writing routes-manifest.json

* Add support for dynamicRoutes

* Add another test with multiple named params
2021-11-24 18:00:12 -05:00
178 changed files with 25906 additions and 4133 deletions

40
.github/CODEOWNERS vendored
View File

@@ -4,24 +4,26 @@
* @TooTallNate * @TooTallNate
/.github/workflows @AndyBitz @styfle /.github/workflows @AndyBitz @styfle
/packages/frameworks @AndyBitz /packages/frameworks @AndyBitz
/packages/cli/src/commands/dev @TooTallNate @styfle @AndyBitz /packages/cli/src/commands/build @TooTallNate @styfle @AndyBitz @gdborton @jaredpalmer
/packages/cli/src/util/dev @TooTallNate @styfle @AndyBitz /packages/cli/src/commands/dev @TooTallNate @styfle @AndyBitz
/packages/cli/src/commands/domains @javivelasco @mglagola @anatrajkovska /packages/cli/src/util/dev @TooTallNate @styfle @AndyBitz
/packages/cli/src/commands/certs @javivelasco @mglagola @anatrajkovska /packages/cli/src/commands/domains @javivelasco @mglagola @anatrajkovska
/packages/cli/src/commands/env @styfle @lucleray /packages/cli/src/commands/certs @javivelasco @mglagola @anatrajkovska
/packages/client @rdev @styfle @TooTallNate /packages/cli/src/commands/env @styfle @lucleray
/packages/build-utils @styfle @AndyBitz @TooTallNate /packages/client @styfle @TooTallNate
/packages/node @styfle @TooTallNate @lucleray /packages/build-utils @styfle @AndyBitz @TooTallNate
/packages/node-bridge @styfle @TooTallNate @lucleray /packages/middleware @gdborton @javivelasco
/packages/next @Timer @ijjk /packages/node @styfle @TooTallNate @lucleray
/packages/go @styfle @TooTallNate /packages/node-bridge @styfle @TooTallNate @lucleray
/packages/python @styfle @TooTallNate /packages/next @Timer @ijjk
/packages/ruby @styfle @coetry @TooTallNate /packages/go @styfle @TooTallNate
/packages/static-build @styfle @AndyBitz /packages/python @styfle @TooTallNate
/packages/routing-utils @styfle @dav-is @ijjk /packages/ruby @styfle @TooTallNate
/examples @mcsdevv @timothyis /packages/static-build @styfle @AndyBitz
/packages/routing-utils @styfle @dav-is @ijjk
/examples @mcsdevv
/examples/create-react-app @Timer /examples/create-react-app @Timer
/examples/nextjs @timneutkens @Timer /examples/nextjs @timneutkens @Timer
/examples/hugo @mcsdevv @timothyis @styfle /examples/hugo @mcsdevv @styfle
/examples/jekyll @mcsdevv @timothyis @styfle /examples/jekyll @mcsdevv @styfle
/examples/zola @mcsdevv @timothyis @styfle /examples/zola @mcsdevv @styfle

View File

@@ -5,7 +5,7 @@
"description": "API for the vercel/vercel repo", "description": "API for the vercel/vercel repo",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"vercel-build": "yarn --cwd .. && node ../utils/run.js build all" "vercel-build": "node ../utils/run.js build all"
}, },
"dependencies": { "dependencies": {
"@sentry/node": "5.11.1", "@sentry/node": "5.11.1",

15787
examples/nextjs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
node_modules node_modules
/.cache .cache
/.vercel .vercel
/.output .output
/public/build public/build
/api/build api/_build

View File

@@ -2,56 +2,33 @@
- [Remix Docs](https://remix.run/docs) - [Remix Docs](https://remix.run/docs)
## Vercel Setup ## Deployment
First you'll need the [Vercel CLI](https://vercel.com/docs/cli): After having run the `create-remix` command and selected "Vercel" as a deployment target, you only need to [import your Git repository](https://vercel.com/new) into Vercel, and it will be deployed.
If you'd like to avoid using a Git repository, you can also deploy the directory by running [Vercel CLI](https://vercel.com/cli):
```sh ```sh
npm i -g vercel npm i -g vercel
vercel
``` ```
Before you can run the app in development, you need to link this project to a new Vercel project on your account. It is generally recommended to use a Git repository, because future commits will then automatically be deployed by Vercel, through its [Git Integration](https://vercel.com/docs/concepts/git).
**It is important that you use a new project. If you try to link this project to an existing project (like a Next.js site) you will have problems.**
```sh
$ vercel link
```
Follow the prompts, and when it's done you should be able to get started.
## Development ## Development
You will be running two processes during development when using Vercel as your server. To run your Remix app locally, make sure your project's local dependencies are installed:
- Your Vercel server in one
- The Remix development server in another
```sh ```sh
# in one tab npm install
$ vercel dev ```
# in another Afterwards, start the Remix development server like so:
$ npm run dev
```sh
npm run dev
``` ```
Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go! Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go!
If you'd rather run everything in a single tab, you can look at [concurrently](https://npm.im/concurrently) or similar tools to run both processes in one tab. If you're used to using the `vercel dev` command provided by [Vercel CLI](https://vercel.com/cli) instead, you can also use that, but it's not needed.
## Deploying
```sh
$ npm run build
# preview deployment
$ vercel
# production deployment
$ vercel --prod
```
### GitHub Automatic Deployments
For some reason the GitHub integration doesn't deploy the public folder. We're working with Vercel to figure this out.
For now, [you can set up a GitHub action with this config](https://gist.github.com/mcansh/91f8effda798b41bb373351fad217070) from our friend [@mcansh](https://github.com/mcansh).

View File

@@ -1,5 +1,5 @@
const { createRequestHandler } = require("@remix-run/vercel"); const { createRequestHandler } = require("@remix-run/vercel");
module.exports = createRequestHandler({ module.exports = createRequestHandler({
build: require("./build") build: require("./_build")
}); });

View File

@@ -1,4 +1,3 @@
import * as React from "react";
import { import {
Link, Link,
Links, Links,
@@ -7,23 +6,14 @@ import {
Outlet, Outlet,
Scripts, Scripts,
ScrollRestoration, ScrollRestoration,
useCatch, useCatch
useLocation
} from "remix"; } from "remix";
import type { LinksFunction } from "remix"; import type { LinksFunction } from "remix";
import deleteMeRemixStyles from "~/styles/demos/remix.css";
import globalStylesUrl from "~/styles/global.css"; import globalStylesUrl from "~/styles/global.css";
import darkStylesUrl from "~/styles/dark.css"; import darkStylesUrl from "~/styles/dark.css";
/** // https://remix.run/api/app#links
* The `links` export is a function that returns an array of objects that map to
* the attributes for an HTML `<link>` element. These will load `<link>` tags on
* every route in the app, but individual routes can include their own links
* that are automatically unloaded when a user navigates away from the route.
*
* https://remix.run/api/app#links
*/
export let links: LinksFunction = () => { export let links: LinksFunction = () => {
return [ return [
{ rel: "stylesheet", href: globalStylesUrl }, { rel: "stylesheet", href: globalStylesUrl },
@@ -31,16 +21,12 @@ export let links: LinksFunction = () => {
rel: "stylesheet", rel: "stylesheet",
href: darkStylesUrl, href: darkStylesUrl,
media: "(prefers-color-scheme: dark)" media: "(prefers-color-scheme: dark)"
}, }
{ rel: "stylesheet", href: deleteMeRemixStyles }
]; ];
}; };
/** // https://remix.run/api/conventions#default-export
* The root module's default export is a component that renders the current // https://remix.run/api/conventions#route-filenames
* route via the `<Outlet />` component. Think of this as the global layout
* component for your app.
*/
export default function App() { export default function App() {
return ( return (
<Document> <Document>
@@ -51,68 +37,27 @@ export default function App() {
); );
} }
function Document({ // https://remix.run/docs/en/v1/api/conventions#errorboundary
children, export function ErrorBoundary({ error }: { error: Error }) {
title console.error(error);
}: {
children: React.ReactNode;
title?: string;
}) {
return ( return (
<html lang="en"> <Document title="Error!">
<head> <Layout>
<meta charSet="utf-8" /> <div>
<meta name="viewport" content="width=device-width,initial-scale=1" /> <h1>There was an error</h1>
{title ? <title>{title}</title> : null} <p>{error.message}</p>
<Meta /> <hr />
<Links /> <p>
</head> Hey, developer, you should replace this with what you want your
<body> users to see.
{children} </p>
<RouteChangeAnnouncement /> </div>
<ScrollRestoration /> </Layout>
<Scripts /> </Document>
{process.env.NODE_ENV === "development" && <LiveReload />}
</body>
</html>
);
}
function Layout({ children }: React.PropsWithChildren<{}>) {
return (
<div className="remix-app">
<header className="remix-app__header">
<div className="container remix-app__header-content">
<Link to="/" title="Remix" className="remix-app__header-home-link">
<RemixLogo />
</Link>
<nav aria-label="Main navigation" className="remix-app__header-nav">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<a href="https://remix.run/docs">Remix Docs</a>
</li>
<li>
<a href="https://github.com/remix-run/remix">GitHub</a>
</li>
</ul>
</nav>
</div>
</header>
<div className="remix-app__main">
<div className="container remix-app__main-content">{children}</div>
</div>
<footer className="remix-app__footer">
<div className="container remix-app__footer-content">
<p>&copy; You!</p>
</div>
</footer>
</div>
); );
} }
// https://remix.run/docs/en/v1/api/conventions#catchboundary
export function CatchBoundary() { export function CatchBoundary() {
let caught = useCatch(); let caught = useCatch();
@@ -148,26 +93,68 @@ export function CatchBoundary() {
); );
} }
export function ErrorBoundary({ error }: { error: Error }) { function Document({
console.error(error); children,
title
}: {
children: React.ReactNode;
title?: string;
}) {
return ( return (
<Document title="Error!"> <html lang="en">
<Layout> <head>
<div> <meta charSet="utf-8" />
<h1>There was an error</h1> <meta name="viewport" content="width=device-width,initial-scale=1" />
<p>{error.message}</p> {title ? <title>{title}</title> : null}
<hr /> <Meta />
<p> <Links />
Hey, developer, you should replace this with what you want your </head>
users to see. <body>
</p> {children}
</div> <ScrollRestoration />
</Layout> <Scripts />
</Document> {process.env.NODE_ENV === "development" && <LiveReload />}
</body>
</html>
); );
} }
function RemixLogo(props: React.ComponentPropsWithoutRef<"svg">) { function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="remix-app">
<header className="remix-app__header">
<div className="container remix-app__header-content">
<Link to="/" title="Remix" className="remix-app__header-home-link">
<RemixLogo />
</Link>
<nav aria-label="Main navigation" className="remix-app__header-nav">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<a href="https://remix.run/docs">Remix Docs</a>
</li>
<li>
<a href="https://github.com/remix-run/remix">GitHub</a>
</li>
</ul>
</nav>
</div>
</header>
<div className="remix-app__main">
<div className="container remix-app__main-content">{children}</div>
</div>
<footer className="remix-app__footer">
<div className="container remix-app__footer-content">
<p>&copy; You!</p>
</div>
</footer>
</div>
);
}
function RemixLogo() {
return ( return (
<svg <svg
viewBox="0 0 659 165" viewBox="0 0 659 165"
@@ -179,7 +166,6 @@ function RemixLogo(props: React.ComponentPropsWithoutRef<"svg">) {
width="106" width="106"
height="30" height="30"
fill="currentColor" fill="currentColor"
{...props}
> >
<title id="remix-run-logo-title">Remix Logo</title> <title id="remix-run-logo-title">Remix Logo</title>
<path d="M0 161V136H45.5416C53.1486 136 54.8003 141.638 54.8003 145V161H0Z M133.85 124.16C135.3 142.762 135.3 151.482 135.3 161H92.2283C92.2283 158.927 92.2653 157.03 92.3028 155.107C92.4195 149.128 92.5411 142.894 91.5717 130.304C90.2905 111.872 82.3473 107.776 67.7419 107.776H54.8021H0V74.24H69.7918C88.2407 74.24 97.4651 68.632 97.4651 53.784C97.4651 40.728 88.2407 32.816 69.7918 32.816H0V0H77.4788C119.245 0 140 19.712 140 51.2C140 74.752 125.395 90.112 105.665 92.672C122.32 96 132.057 105.472 133.85 124.16Z" /> <path d="M0 161V136H45.5416C53.1486 136 54.8003 141.638 54.8003 145V161H0Z M133.85 124.16C135.3 142.762 135.3 151.482 135.3 161H92.2283C92.2283 158.927 92.2653 157.03 92.3028 155.107C92.4195 149.128 92.5411 142.894 91.5717 130.304C90.2905 111.872 82.3473 107.776 67.7419 107.776H54.8021H0V74.24H69.7918C88.2407 74.24 97.4651 68.632 97.4651 53.784C97.4651 40.728 88.2407 32.816 69.7918 32.816H0V0H77.4788C119.245 0 140 19.712 140 51.2C140 74.752 125.395 90.112 105.665 92.672C122.32 96 132.057 105.472 133.85 124.16Z" />
@@ -190,58 +176,3 @@ function RemixLogo(props: React.ComponentPropsWithoutRef<"svg">) {
</svg> </svg>
); );
} }
/**
* Provides an alert for screen reader users when the route changes.
*/
const RouteChangeAnnouncement = React.memo(() => {
let [hydrated, setHydrated] = React.useState(false);
let [innerHtml, setInnerHtml] = React.useState("");
let location = useLocation();
React.useEffect(() => {
setHydrated(true);
}, []);
let firstRenderRef = React.useRef(true);
React.useEffect(() => {
// Skip the first render because we don't want an announcement on the
// initial page load.
if (firstRenderRef.current) {
firstRenderRef.current = false;
return;
}
let pageTitle = location.pathname === "/" ? "Home page" : document.title;
setInnerHtml(`Navigated to ${pageTitle}`);
}, [location.pathname]);
// Render nothing on the server. The live region provides no value unless
// scripts are loaded and the browser takes over normal routing.
if (!hydrated) {
return null;
}
return (
<div
aria-live="assertive"
aria-atomic
id="route-change-region"
style={{
border: "0",
clipPath: "inset(100%)",
clip: "rect(0 0 0 0)",
height: "1px",
margin: "-1px",
overflow: "hidden",
padding: "0",
position: "absolute",
width: "1px",
whiteSpace: "nowrap",
wordWrap: "normal"
}}
>
{innerHtml}
</div>
);
});

View File

@@ -1,120 +0,0 @@
/*
* You probably want to just delete this file; it's just for the demo pages.
*/
.remix-app {
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: calc(100vh - env(safe-area-inset-bottom));
}
.remix-app > * {
width: 100%;
}
.remix-app__header {
padding-top: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.remix-app__header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.remix-app__header-home-link {
width: 106px;
height: 30px;
color: var(--color-foreground);
}
.remix-app__header-nav ul {
list-style: none;
margin: 0;
display: flex;
align-items: center;
gap: 1.5em;
}
.remix-app__header-nav li {
font-weight: bold;
}
.remix-app__main {
flex: 1 1 100%;
}
.remix-app__footer {
padding-top: 1rem;
padding-bottom: 1rem;
border-top: 1px solid var(--color-border);
}
.remix-app__footer-content {
display: flex;
justify-content: center;
align-items: center;
}
.remix__page {
--gap: 1rem;
--space: 2rem;
display: grid;
grid-auto-rows: min-content;
gap: var(--gap);
padding-top: var(--space);
padding-bottom: var(--space);
}
@media print, screen and (min-width: 640px) {
.remix__page {
--gap: 2rem;
grid-auto-rows: unset;
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (min-width: 1024px) {
.remix__page {
--gap: 4rem;
}
}
.remix__page > main > :first-child {
margin-top: 0;
}
.remix__page > main > :last-child {
margin-bottom: 0;
}
.remix__page > aside {
margin: 0;
padding: 1.5ch 2ch;
border: solid 1px var(--color-border);
border-radius: 0.5rem;
}
.remix__page > aside > :first-child {
margin-top: 0;
}
.remix__page > aside > :last-child {
margin-bottom: 0;
}
.remix__form {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
}
.remix__form > * {
margin-top: 0;
margin-bottom: 0;
}

View File

@@ -96,3 +96,121 @@ input:where([type="search"]) {
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
} }
.remix-app {
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: calc(100vh - env(safe-area-inset-bottom));
}
.remix-app > * {
width: 100%;
}
.remix-app__header {
padding-top: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.remix-app__header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.remix-app__header-home-link {
width: 106px;
height: 30px;
color: var(--color-foreground);
}
.remix-app__header-nav ul {
list-style: none;
margin: 0;
display: flex;
align-items: center;
gap: 1.5em;
}
.remix-app__header-nav li {
font-weight: bold;
}
.remix-app__main {
flex: 1 1 100%;
}
.remix-app__footer {
padding-top: 1rem;
padding-bottom: 1rem;
border-top: 1px solid var(--color-border);
}
.remix-app__footer-content {
display: flex;
justify-content: center;
align-items: center;
}
.remix__page {
--gap: 1rem;
--space: 2rem;
display: grid;
grid-auto-rows: min-content;
gap: var(--gap);
padding-top: var(--space);
padding-bottom: var(--space);
}
@media print, screen and (min-width: 640px) {
.remix__page {
--gap: 2rem;
grid-auto-rows: unset;
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (min-width: 1024px) {
.remix__page {
--gap: 4rem;
}
}
.remix__page > main > :first-child {
margin-top: 0;
}
.remix__page > main > :last-child {
margin-bottom: 0;
}
.remix__page > aside {
margin: 0;
padding: 1.5ch 2ch;
border: solid 1px var(--color-border);
border-radius: 0.5rem;
}
.remix__page > aside > :first-child {
margin-top: 0;
}
.remix__page > aside > :last-child {
margin-bottom: 0;
}
.remix__form {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
}
.remix__form > * {
margin-top: 0;
margin-bottom: 0;
}

8345
examples/remix/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,15 @@
"postinstall": "remix setup node" "postinstall": "remix setup node"
}, },
"dependencies": { "dependencies": {
"@remix-run/react": "^1.0.4", "@remix-run/react": "^1.0.6",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"remix": "^1.0.4", "remix": "^1.0.6",
"@remix-run/vercel": "^1.0.4" "@remix-run/serve": "^1.0.6",
"@remix-run/vercel": "^1.0.6"
}, },
"devDependencies": { "devDependencies": {
"@remix-run/dev": "^1.0.4", "@remix-run/dev": "^1.0.6",
"@types/react": "^17.0.24", "@types/react": "^17.0.24",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"typescript": "^4.1.2" "typescript": "^4.1.2"

View File

@@ -5,5 +5,5 @@ module.exports = {
appDirectory: "app", appDirectory: "app",
browserBuildDirectory: "public/build", browserBuildDirectory: "public/build",
publicPath: "/build/", publicPath: "/build/",
serverBuildDirectory: "api/build" serverBuildDirectory: "api/_build"
}; };

View File

@@ -1,7 +1,7 @@
{ {
"build": { "build": {
"env": { "env": {
"ENABLE_FILE_SYSTEM_API": "1" "ENABLE_FILE_SYSTEM_API": "1"
}
} }
} }
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/build-utils", "name": "@vercel/build-utils",
"version": "2.12.3-canary.21", "version": "2.12.3-canary.42",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.js", "types": "./dist/index.d.js",
@@ -30,7 +30,7 @@
"@types/node-fetch": "^2.1.6", "@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0", "@types/semver": "6.0.0",
"@types/yazl": "^2.4.1", "@types/yazl": "^2.4.1",
"@vercel/frameworks": "0.5.1-canary.13", "@vercel/frameworks": "0.5.1-canary.17",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1", "aggregate-error": "3.0.1",
"async-retry": "1.2.3", "async-retry": "1.2.3",

View File

@@ -1,124 +1,296 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import { join, dirname, relative } from 'path'; import { join, parse, relative, dirname, basename, extname } from 'path';
import glob from './fs/glob'; import glob from './fs/glob';
import { normalizePath } from './fs/normalize-path'; import { normalizePath } from './fs/normalize-path';
import { FILES_SYMBOL, getLambdaOptionsFromFunction, Lambda } from './lambda'; import { FILES_SYMBOL, Lambda } from './lambda';
import type FileBlob from './file-blob'; import type { BuildOptions, Files } from './types';
import type { BuilderFunctions, BuildOptions, Files } from './types'; import { debug, getIgnoreFilter } from '.';
import minimatch from 'minimatch';
// `.output` was already created by the Build Command, so we have
// to ensure its contents don't get bundled into the Lambda. Similarily,
// we don't want to bundle anything from `.vercel` either. Lastly,
// Builders/Runtimes didn't have `vercel.json` or `now.json`.
const ignoredPaths = ['.output', '.vercel', 'vercel.json', 'now.json'];
const shouldIgnorePath = (
file: string,
ignoreFilter: any,
ignoreFile: boolean
) => {
const isNative = ignoredPaths.some(item => {
return file.startsWith(item);
});
if (!ignoreFile) {
return isNative;
}
return isNative || ignoreFilter(file);
};
const getSourceFiles = async (workPath: string, ignoreFilter: any) => {
const list = await glob('**', {
cwd: workPath,
});
// We're not passing this as an `ignore` filter to the `glob` function above,
// so that we can re-use exactly the same `getIgnoreFilter` method that the
// Build Step uses (literally the same code). Note that this exclusion only applies
// when deploying. Locally, another exclusion is needed, which is handled
// further below in the `convertRuntimeToPlugin` function.
for (const file in list) {
if (shouldIgnorePath(file, ignoreFilter, true)) {
delete list[file];
}
}
return list;
};
/** /**
* Convert legacy Runtime to a Plugin. * Convert legacy Runtime to a Plugin.
* @param buildRuntime - a legacy build() function from a Runtime * @param buildRuntime - a legacy build() function from a Runtime
* @param packageName - the name of the package, for example `vercel-plugin-python`
* @param ext - the file extension, for example `.py` * @param ext - the file extension, for example `.py`
*/ */
export function convertRuntimeToPlugin( export function convertRuntimeToPlugin(
buildRuntime: (options: BuildOptions) => Promise<{ output: Lambda }>, buildRuntime: (options: BuildOptions) => Promise<{ output: Lambda }>,
packageName: string,
ext: string ext: string
) { ) {
// This `build()` signature should match `plugin.build()` signature in `vercel build`. // This `build()` signature should match `plugin.build()` signature in `vercel build`.
return async function build({ return async function build({ workPath }: { workPath: string }) {
vercelConfig, // We also don't want to provide any files to Runtimes that were ignored
workPath, // through `.vercelignore` or `.nowignore`, because the Build Step does the same.
}: { const ignoreFilter = await getIgnoreFilter(workPath);
vercelConfig: { functions?: BuilderFunctions; regions?: string[] };
workPath: string; // Retrieve the files that are currently available on the File System,
}) { // before the Legacy Runtime has even started to build.
const opts = { cwd: workPath }; const sourceFilesPreBuild = await getSourceFiles(workPath, ignoreFilter);
const files = await glob('**', opts);
delete files['vercel.json']; // Builders/Runtimes didn't have vercel.json // Instead of doing another `glob` to get all the matching source files,
const entrypoints = await glob(`api/**/*${ext}`, opts); // we'll filter the list of existing files down to only the ones
// that are matching the entrypoint pattern, so we're first creating
// a clean new list to begin.
const entrypoints = Object.assign({}, sourceFilesPreBuild);
const entrypointMatch = new RegExp(`^api/.*${ext}$`);
// Up next, we'll strip out the files from the list of entrypoints
// that aren't actually considered entrypoints.
for (const file in entrypoints) {
if (!entrypointMatch.test(file)) {
delete entrypoints[file];
}
}
const pages: { [key: string]: any } = {}; const pages: { [key: string]: any } = {};
const { functions = {} } = vercelConfig; const pluginName = packageName.replace('vercel-plugin-', '');
const traceDir = join(workPath, '.output', 'runtime-traced-files'); const outputPath = join(workPath, '.output');
const traceDir = join(
outputPath,
`inputs`,
// Legacy Runtimes can only provide API Routes, so that's
// why we can use this prefix for all of them. Here, we have to
// make sure to not use a cryptic hash name, because people
// need to be able to easily inspect the output.
`api-routes-${pluginName}`
);
await fs.ensureDir(traceDir); await fs.ensureDir(traceDir);
for (const entrypoint of Object.keys(entrypoints)) { const entryRoot = join(outputPath, 'server', 'pages');
const key =
Object.keys(functions).find(
src => src === entrypoint || minimatch(entrypoint, src)
) || '';
const config = functions[key] || {};
for (const entrypoint of Object.keys(entrypoints)) {
const { output } = await buildRuntime({ const { output } = await buildRuntime({
files, files: sourceFilesPreBuild,
entrypoint, entrypoint,
workPath, workPath,
config: { config: {
zeroConfig: true, zeroConfig: true,
includeFiles: config.includeFiles, },
excludeFiles: config.excludeFiles, meta: {
avoidTopLevelInstall: true,
skipDownload: true,
}, },
}); });
pages[entrypoint] = { // @ts-ignore This symbol is a private API
handler: output.handler, const lambdaFiles: Files = output[FILES_SYMBOL];
// When deploying, the `files` that are passed to the Legacy Runtimes already
// have certain files that are ignored stripped, but locally, that list of
// files isn't used by the Legacy Runtimes, so we need to apply the filters
// to the outputs that they are returning instead.
for (const file in lambdaFiles) {
if (shouldIgnorePath(file, ignoreFilter, false)) {
delete lambdaFiles[file];
}
}
let handlerFileBase = output.handler;
let handlerFile = lambdaFiles[handlerFileBase];
let handlerHasImport = false;
const { handler } = output;
const handlerMethod = handler.split('.').pop();
const handlerFileName = handler.replace(`.${handlerMethod}`, '');
// For compiled languages, the launcher file for the Lambda generated
// by the Legacy Runtime matches the `handler` defined for it, but for
// interpreted languages, the `handler` consists of the launcher file name
// without an extension, plus the name of the method inside of that file
// that should be invoked, so we have to construct the file path explicitly.
if (!handlerFile) {
handlerFileBase = handlerFileName + ext;
handlerFile = lambdaFiles[handlerFileBase];
handlerHasImport = true;
}
if (!handlerFile || !handlerFile.fsPath) {
throw new Error(
`Could not find a handler file. Please ensure that \`files\` for the returned \`Lambda\` contains an \`FileFsRef\` named "${handlerFileBase}" with a valid \`fsPath\`.`
);
}
const handlerExtName = extname(handlerFile.fsPath);
const entryBase = basename(entrypoint).replace(ext, handlerExtName);
const entryPath = join(dirname(entrypoint), entryBase);
const entry = join(entryRoot, entryPath);
// Create the parent directory of the API Route that will be created
// for the current entrypoint inside of `.output/server/pages/api`.
await fs.ensureDir(dirname(entry));
// For compiled languages, the launcher file will be binary and therefore
// won't try to import a user-provided request handler (instead, it will
// contain it). But for interpreted languages, the launcher might try to
// load a user-provided request handler from the source file instead of bundling
// it, so we have to adjust the import statement inside the launcher to point
// to the respective source file. Previously, Legacy Runtimes simply expected
// the user-provided request-handler to be copied right next to the launcher,
// but with the new File System API, files won't be moved around unnecessarily.
if (handlerHasImport) {
const { fsPath } = handlerFile;
const encoding = 'utf-8';
// This is the true directory of the user-provided request handler in the
// source files, so that's what we will use as an import path in the launcher.
const locationPrefix = relative(entry, outputPath);
let handlerContent = await fs.readFile(fsPath, encoding);
const importPaths = [
// This is the full entrypoint path, like `./api/test.py`. In our tests
// Python didn't support importing from a parent directory without using different
// code in the launcher that registers it as a location for modules and then changing
// the importing syntax, but continuing to import it like before seems to work. If
// other languages need this, we should consider excluding Python explicitly.
// `./${entrypoint}`,
// This is the entrypoint path without extension, like `api/test`
entrypoint.slice(0, -ext.length),
];
// Generate a list of regular expressions that we can use for
// finding matches, but only allow matches if the import path is
// wrapped inside single (') or double quotes (").
const patterns = importPaths.map(path => {
// eslint-disable-next-line no-useless-escape
return new RegExp(`('|")(${path.replace(/\./g, '\\.')})('|")`, 'g');
});
let replacedMatch = null;
for (const pattern of patterns) {
const newContent = handlerContent.replace(
pattern,
(_, p1, p2, p3) => {
return `${p1}${join(locationPrefix, p2)}${p3}`;
}
);
if (newContent !== handlerContent) {
debug(
`Replaced "${pattern}" inside "${entry}" to ensure correct import of user-provided request handler`
);
handlerContent = newContent;
replacedMatch = true;
}
}
if (!replacedMatch) {
new Error(
`No replacable matches for "${importPaths[0]}" or "${importPaths[1]}" found in "${fsPath}"`
);
}
await fs.writeFile(entry, handlerContent, encoding);
} else {
await fs.copy(handlerFile.fsPath, entry);
}
// Legacy Runtimes based on interpreted languages will create a new launcher file
// for every entrypoint, but they will create each one inside `workPath`, which means that
// the launcher for one entrypoint will overwrite the launcher provided for the previous
// entrypoint. That's why, above, we copy the file contents into the new destination (and
// optionally transform them along the way), instead of linking. We then also want to remove
// the copy origin right here, so that the `workPath` doesn't contain a useless launcher file
// once the build has finished running.
await fs.remove(handlerFile.fsPath);
debug(`Removed temporary file "${handlerFile.fsPath}"`);
const nft = `${entry}.nft.json`;
const json = JSON.stringify({
version: 2,
files: Object.keys(lambdaFiles)
.map(file => {
const { fsPath } = lambdaFiles[file];
if (!fsPath) {
throw new Error(
`File "${file}" is missing valid \`fsPath\` property`
);
}
// The handler was already moved into position above.
if (file === handlerFileBase) {
return;
}
return normalizePath(relative(dirname(nft), fsPath));
})
.filter(Boolean),
});
await fs.writeFile(nft, json);
// Add an entry that will later on be added to the `functions-manifest.json`
// file that is placed inside of the `.output` directory.
pages[normalizePath(entryPath)] = {
// Because the underlying file used as a handler was placed
// inside `.output/server/pages/api`, it no longer has the name it originally
// had and is now named after the API Route that it's responsible for,
// so we have to adjust the name of the Lambda handler accordingly.
handler: handler.replace(handlerFileName, parse(entry).name),
runtime: output.runtime, runtime: output.runtime,
memory: output.memory, memory: output.memory,
maxDuration: output.maxDuration, maxDuration: output.maxDuration,
environment: output.environment, environment: output.environment,
allowQuery: output.allowQuery, allowQuery: output.allowQuery,
regions: output.regions,
}; };
// @ts-ignore This symbol is a private API
const lambdaFiles: Files = output[FILES_SYMBOL];
const entry = join(workPath, '.output', 'server', 'pages', entrypoint);
await fs.ensureDir(dirname(entry));
await linkOrCopy(files[entrypoint].fsPath, entry);
const tracedFiles: {
absolutePath: string;
relativePath: string;
}[] = [];
Object.entries(lambdaFiles).forEach(async ([relPath, file]) => {
const newPath = join(traceDir, relPath);
tracedFiles.push({ absolutePath: newPath, relativePath: relPath });
if (file.fsPath) {
await linkOrCopy(file.fsPath, newPath);
} else if (file.type === 'FileBlob') {
const { data, mode } = file as FileBlob;
await fs.writeFile(newPath, data, { mode });
} else {
throw new Error(`Unknown file type: ${file.type}`);
}
});
const nft = join(
workPath,
'.output',
'server',
'pages',
`${entrypoint}.nft.json`
);
const json = JSON.stringify({
version: 1,
files: tracedFiles.map(f => ({
input: normalizePath(relative(nft, f.absolutePath)),
output: normalizePath(f.relativePath),
})),
});
await fs.ensureDir(dirname(nft));
await fs.writeFile(nft, json);
} }
await updateFunctionsManifest({ vercelConfig, workPath, pages }); // Add any Serverless Functions that were exposed by the Legacy Runtime
// to the `functions-manifest.json` file provided in `.output`.
await updateFunctionsManifest({ workPath, pages });
}; };
} }
async function linkOrCopy(existingPath: string, newPath: string) {
try {
await fs.createLink(existingPath, newPath);
} catch (err: any) {
if (err.code !== 'EEXIST') {
await fs.copyFile(existingPath, newPath);
}
}
}
async function readJson(filePath: string): Promise<{ [key: string]: any }> { async function readJson(filePath: string): Promise<{ [key: string]: any }> {
try { try {
const str = await fs.readFile(filePath, 'utf8'); const str = await fs.readFile(filePath, 'utf8');
@@ -133,15 +305,12 @@ async function readJson(filePath: string): Promise<{ [key: string]: any }> {
/** /**
* If `.output/functions-manifest.json` exists, append to the pages * If `.output/functions-manifest.json` exists, append to the pages
* property. Otherwise write a new file. This will also read `vercel.json` * property. Otherwise write a new file.
* and apply relevant `functions` property config.
*/ */
export async function updateFunctionsManifest({ export async function updateFunctionsManifest({
vercelConfig,
workPath, workPath,
pages, pages,
}: { }: {
vercelConfig: { functions?: BuilderFunctions; regions?: string[] };
workPath: string; workPath: string;
pages: { [key: string]: any }; pages: { [key: string]: any };
}) { }) {
@@ -152,52 +321,92 @@ export async function updateFunctionsManifest({
); );
const functionsManifest = await readJson(functionsManifestPath); const functionsManifest = await readJson(functionsManifestPath);
if (!functionsManifest.version) functionsManifest.version = 1; if (!functionsManifest.version) functionsManifest.version = 2;
if (!functionsManifest.pages) functionsManifest.pages = {}; if (!functionsManifest.pages) functionsManifest.pages = {};
for (const [pageKey, pageConfig] of Object.entries(pages)) { for (const [pageKey, pageConfig] of Object.entries(pages)) {
const fnConfig = await getLambdaOptionsFromFunction({ functionsManifest.pages[pageKey] = { ...pageConfig };
sourceFile: pageKey,
config: vercelConfig,
});
functionsManifest.pages[pageKey] = {
...pageConfig,
memory: fnConfig.memory || pageConfig.memory,
maxDuration: fnConfig.maxDuration || pageConfig.maxDuration,
regions: vercelConfig.regions || pageConfig.regions,
};
} }
await fs.writeFile(functionsManifestPath, JSON.stringify(functionsManifest)); await fs.writeFile(functionsManifestPath, JSON.stringify(functionsManifest));
} }
/** /**
* Will append routes to the `routes-manifest.json` file. * Append routes to the `routes-manifest.json` file.
* If the file does not exist, it'll be created. * If the file does not exist, it will be created.
*/ */
export async function updateRoutesManifest({ export async function updateRoutesManifest({
workPath, workPath,
redirects,
rewrites,
headers,
dynamicRoutes, dynamicRoutes,
staticRoutes,
}: { }: {
workPath: string; workPath: string;
redirects?: {
source: string;
destination: string;
statusCode: number;
regex: string;
}[];
rewrites?: {
source: string;
destination: string;
regex: string;
}[];
headers?: {
source: string;
headers: {
key: string;
value: string;
}[];
regex: string;
}[];
dynamicRoutes?: { dynamicRoutes?: {
page: string; page: string;
regex: string; regex: string;
namedRegex?: string; namedRegex?: string;
routeKeys?: { [named: string]: string }; routeKeys?: { [named: string]: string };
}[]; }[];
staticRoutes?: {
page: string;
regex: string;
namedRegex?: string;
routeKeys?: { [named: string]: string };
}[];
}) { }) {
const routesManifestPath = join(workPath, '.output', 'routes-manifest.json'); const routesManifestPath = join(workPath, '.output', 'routes-manifest.json');
const routesManifest = await readJson(routesManifestPath); const routesManifest = await readJson(routesManifestPath);
if (!routesManifest.version) routesManifest.version = 1; if (!routesManifest.version) routesManifest.version = 3;
if (routesManifest.pages404 === undefined) routesManifest.pages404 = true; if (routesManifest.pages404 === undefined) routesManifest.pages404 = true;
if (redirects) {
if (!routesManifest.redirects) routesManifest.redirects = [];
routesManifest.redirects.push(...redirects);
}
if (rewrites) {
if (!routesManifest.rewrites) routesManifest.rewrites = [];
routesManifest.rewrites.push(...rewrites);
}
if (headers) {
if (!routesManifest.headers) routesManifest.headers = [];
routesManifest.headers.push(...headers);
}
if (dynamicRoutes) { if (dynamicRoutes) {
if (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = []; if (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = [];
routesManifest.dynamicRoutes.push(...dynamicRoutes); routesManifest.dynamicRoutes.push(...dynamicRoutes);
} }
if (staticRoutes) {
if (!routesManifest.staticRoutes) routesManifest.staticRoutes = [];
routesManifest.staticRoutes.push(...staticRoutes);
}
await fs.writeFile(routesManifestPath, JSON.stringify(routesManifest)); await fs.writeFile(routesManifestPath, JSON.stringify(routesManifest));
} }

View File

@@ -96,6 +96,7 @@ export async function detectBuilders(
redirectRoutes: Route[] | null; redirectRoutes: Route[] | null;
rewriteRoutes: Route[] | null; rewriteRoutes: Route[] | null;
errorRoutes: Route[] | null; errorRoutes: Route[] | null;
limitedRoutes: LimitedRoutes | null;
}> { }> {
const errors: ErrorResponse[] = []; const errors: ErrorResponse[] = [];
const warnings: ErrorResponse[] = []; const warnings: ErrorResponse[] = [];
@@ -114,6 +115,7 @@ export async function detectBuilders(
redirectRoutes: null, redirectRoutes: null,
rewriteRoutes: null, rewriteRoutes: null,
errorRoutes: null, errorRoutes: null,
limitedRoutes: null,
}; };
} }
@@ -179,6 +181,7 @@ export async function detectBuilders(
redirectRoutes: null, redirectRoutes: null,
rewriteRoutes: null, rewriteRoutes: null,
errorRoutes: null, errorRoutes: null,
limitedRoutes: null,
}; };
} }
@@ -257,6 +260,7 @@ export async function detectBuilders(
defaultRoutes: null, defaultRoutes: null,
rewriteRoutes: null, rewriteRoutes: null,
errorRoutes: null, errorRoutes: null,
limitedRoutes: null,
}; };
} }
@@ -299,6 +303,7 @@ export async function detectBuilders(
defaultRoutes: null, defaultRoutes: null,
rewriteRoutes: null, rewriteRoutes: null,
errorRoutes: null, errorRoutes: null,
limitedRoutes: null,
}; };
} }
@@ -326,6 +331,7 @@ export async function detectBuilders(
} }
const routesResult = getRouteResult( const routesResult = getRouteResult(
pkg,
apiRoutes, apiRoutes,
dynamicRoutes, dynamicRoutes,
usedOutputDirectory, usedOutputDirectory,
@@ -342,6 +348,7 @@ export async function detectBuilders(
defaultRoutes: routesResult.defaultRoutes, defaultRoutes: routesResult.defaultRoutes,
rewriteRoutes: routesResult.rewriteRoutes, rewriteRoutes: routesResult.rewriteRoutes,
errorRoutes: routesResult.errorRoutes, errorRoutes: routesResult.errorRoutes,
limitedRoutes: routesResult.limitedRoutes,
}; };
} }
@@ -932,7 +939,14 @@ function createRouteFromPath(
return { route, isDynamic }; return { route, isDynamic };
} }
interface LimitedRoutes {
defaultRoutes: Route[];
redirectRoutes: Route[];
rewriteRoutes: Route[];
}
function getRouteResult( function getRouteResult(
pkg: PackageJson | undefined | null,
apiRoutes: Source[], apiRoutes: Source[],
dynamicRoutes: Source[], dynamicRoutes: Source[],
outputDirectory: string, outputDirectory: string,
@@ -944,11 +958,18 @@ function getRouteResult(
redirectRoutes: Route[]; redirectRoutes: Route[];
rewriteRoutes: Route[]; rewriteRoutes: Route[];
errorRoutes: Route[]; errorRoutes: Route[];
limitedRoutes: LimitedRoutes;
} { } {
const deps = Object.assign({}, pkg?.dependencies, pkg?.devDependencies);
const defaultRoutes: Route[] = []; const defaultRoutes: Route[] = [];
const redirectRoutes: Route[] = []; const redirectRoutes: Route[] = [];
const rewriteRoutes: Route[] = []; const rewriteRoutes: Route[] = [];
const errorRoutes: Route[] = []; const errorRoutes: Route[] = [];
const limitedRoutes: LimitedRoutes = {
defaultRoutes: [],
redirectRoutes: [],
rewriteRoutes: [],
};
const framework = frontendBuilder?.config?.framework || ''; const framework = frontendBuilder?.config?.framework || '';
const isNextjs = const isNextjs =
framework === 'nextjs' || isOfficialRuntime('next', frontendBuilder?.use); framework === 'nextjs' || isOfficialRuntime('next', frontendBuilder?.use);
@@ -956,14 +977,43 @@ function getRouteResult(
if (apiRoutes && apiRoutes.length > 0) { if (apiRoutes && apiRoutes.length > 0) {
if (options.featHandleMiss) { if (options.featHandleMiss) {
// Exclude extension names if the corresponding plugin is not found in package.json
// detectBuilders({ignoreRoutesForBuilders: ['@vercel/python']})
// return a copy of routes.
// We should exclud errorRoutes and
const extSet = detectApiExtensions(apiBuilders); const extSet = detectApiExtensions(apiBuilders);
const withTag = options.tag ? `@${options.tag}` : '';
const extSetLimited = detectApiExtensions(
apiBuilders.filter(b => {
if (
b.use === `@vercel/python${withTag}` &&
!('vercel-plugin-python' in deps)
) {
return false;
}
if (
b.use === `@vercel/go${withTag}` &&
!('vercel-plugin-go' in deps)
) {
return false;
}
if (
b.use === `@vercel/ruby${withTag}` &&
!('vercel-plugin-ruby' in deps)
) {
return false;
}
return true;
})
);
if (extSet.size > 0) { if (extSet.size > 0) {
const exts = Array.from(extSet) const extGroup = `(?:\\.(?:${Array.from(extSet)
.map(ext => ext.slice(1)) .map(ext => ext.slice(1))
.join('|'); .join('|')}))`;
const extGroupLimited = `(?:\\.(?:${Array.from(extSetLimited)
const extGroup = `(?:\\.(?:${exts}))`; .map(ext => ext.slice(1))
.join('|')}))`;
if (options.cleanUrls) { if (options.cleanUrls) {
redirectRoutes.push({ redirectRoutes.push({
@@ -979,6 +1029,20 @@ function getRouteResult(
}, },
status: 308, status: 308,
}); });
limitedRoutes.redirectRoutes.push({
src: `^/(api(?:.+)?)/index${extGroupLimited}?/?$`,
headers: { Location: options.trailingSlash ? '/$1/' : '/$1' },
status: 308,
});
limitedRoutes.redirectRoutes.push({
src: `^/api/(.+)${extGroupLimited}/?$`,
headers: {
Location: options.trailingSlash ? '/api/$1/' : '/api/$1',
},
status: 308,
});
} else { } else {
defaultRoutes.push({ handle: 'miss' }); defaultRoutes.push({ handle: 'miss' });
defaultRoutes.push({ defaultRoutes.push({
@@ -986,10 +1050,18 @@ function getRouteResult(
dest: '/api/$1', dest: '/api/$1',
check: true, check: true,
}); });
limitedRoutes.defaultRoutes.push({ handle: 'miss' });
limitedRoutes.defaultRoutes.push({
src: `^/api/(.+)${extGroupLimited}$`,
dest: '/api/$1',
check: true,
});
} }
} }
rewriteRoutes.push(...dynamicRoutes); rewriteRoutes.push(...dynamicRoutes);
limitedRoutes.rewriteRoutes.push(...dynamicRoutes);
if (typeof ignoreRuntimes === 'undefined') { if (typeof ignoreRuntimes === 'undefined') {
// This route is only necessary to hide the directory listing // This route is only necessary to hide the directory listing
@@ -1040,6 +1112,7 @@ function getRouteResult(
redirectRoutes, redirectRoutes,
rewriteRoutes, rewriteRoutes,
errorRoutes, errorRoutes,
limitedRoutes,
}; };
} }

View File

@@ -0,0 +1,84 @@
import path from 'path';
import fs from 'fs-extra';
import ignore from 'ignore';
interface CodedError extends Error {
code: string;
}
function isCodedError(error: unknown): error is CodedError {
return (
error !== null &&
error !== undefined &&
(error as CodedError).code !== undefined
);
}
function clearRelative(s: string) {
return s.replace(/(\n|^)\.\//g, '$1');
}
export default async function (
downloadPath: string,
rootDirectory?: string | undefined
) {
const readFile = async (p: string) => {
try {
return await fs.readFile(p, 'utf8');
} catch (error: any) {
if (
error.code === 'ENOENT' ||
(error instanceof Error && error.message.includes('ENOENT'))
) {
return undefined;
}
throw error;
}
};
const vercelIgnorePath = path.join(
downloadPath,
rootDirectory || '',
'.vercelignore'
);
const nowIgnorePath = path.join(
downloadPath,
rootDirectory || '',
'.nowignore'
);
const ignoreContents = [];
try {
ignoreContents.push(
...(
await Promise.all([readFile(vercelIgnorePath), readFile(nowIgnorePath)])
).filter(Boolean)
);
} catch (error) {
if (isCodedError(error) && error.code === 'ENOTDIR') {
console.log(`Warning: Cannot read ignore file from ${vercelIgnorePath}`);
} else {
throw error;
}
}
if (ignoreContents.length === 2) {
throw new Error(
'Cannot use both a `.vercelignore` and `.nowignore` file. Please delete the `.nowignore` file.'
);
}
if (ignoreContents.length === 0) {
return () => false;
}
const ignoreFilter: any = ignore().add(clearRelative(ignoreContents[0]!));
return function (p: string) {
// we should not ignore now.json and vercel.json if it asked to.
// we depend on these files for building the app with sourceless
if (p === 'now.json' || p === 'vercel.json') return false;
return ignoreFilter.test(p).ignored;
};
}

View File

@@ -1,3 +1,4 @@
import { createHash } from 'crypto';
import FileBlob from './file-blob'; import FileBlob from './file-blob';
import FileFsRef from './file-fs-ref'; import FileFsRef from './file-fs-ref';
import FileRef from './file-ref'; import FileRef from './file-ref';
@@ -33,6 +34,7 @@ import { NowBuildError } from './errors';
import streamToBuffer from './fs/stream-to-buffer'; import streamToBuffer from './fs/stream-to-buffer';
import shouldServe from './should-serve'; import shouldServe from './should-serve';
import debug from './debug'; import debug from './debug';
import getIgnoreFilter from './get-ignore-filter';
export { export {
FileBlob, FileBlob,
@@ -70,6 +72,7 @@ export {
isSymbolicLink, isSymbolicLink,
getLambdaOptionsFromFunction, getLambdaOptionsFromFunction,
scanParentDirs, scanParentDirs,
getIgnoreFilter,
}; };
export { export {
@@ -132,3 +135,11 @@ export const getPlatformEnv = (name: string): string | undefined => {
} }
return n; return n;
}; };
/**
* Helper function for generating file or directories names in `.output/inputs`
* for dependencies of files provided to the File System API.
*/
export const getInputHash = (source: Buffer | string): string => {
return createHash('sha1').update(source).digest('hex');
};

View File

@@ -58,6 +58,7 @@ export interface Meta {
filesRemoved?: string[]; filesRemoved?: string[];
env?: Env; env?: Env;
buildEnv?: Env; buildEnv?: Env;
avoidTopLevelInstall?: boolean;
} }
export interface AnalyzeOptions { export interface AnalyzeOptions {

View File

@@ -0,0 +1 @@
# users.rb

View File

@@ -1,9 +1,9 @@
{ {
"functions": { "functions": {
"api/users/post.py": { "api/users.rb": {
"memory": 3008 "memory": 3008
}, },
"api/not-matching-anything.py": { "api/doesnt-exist.rb": {
"memory": 768 "memory": 768
} }
} }

View File

@@ -0,0 +1 @@
# [id].py

View File

@@ -0,0 +1 @@
# project/[aid]/[bid]/index.py

View File

@@ -0,0 +1,7 @@
{
"functions": {
"api/users/post.py": {
"memory": 3008
}
}
}

View File

@@ -2385,13 +2385,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
{ {
const files = ['api/user.go', 'api/team.js', 'api/package.json']; const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { defaultRoutes, rewriteRoutes, errorRoutes } = await detectBuilders( const { defaultRoutes, rewriteRoutes, errorRoutes, limitedRoutes } =
files, await detectBuilders(files, null, {
null,
{
featHandleMiss, featHandleMiss,
} });
);
expect(defaultRoutes).toStrictEqual([ expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' }, { handle: 'miss' },
{ {
@@ -2414,6 +2411,22 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
}, },
]); ]);
// Limited routes should have js but not go since the go plugin is not installed
expect(limitedRoutes).toStrictEqual({
redirectRoutes: [],
rewriteRoutes: [],
defaultRoutes: [
{
handle: 'miss',
},
{
src: '^/api/(.+)(?:\\.(?:js))$',
dest: '/api/$1',
check: true,
},
],
});
const pattern = new RegExp(errorRoutes![0].src!); const pattern = new RegExp(errorRoutes![0].src!);
[ [
@@ -2816,8 +2829,13 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{ {
const files = ['api/user.go', 'api/team.js', 'api/package.json']; const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { defaultRoutes, redirectRoutes, rewriteRoutes, errorRoutes } = const {
await detectBuilders(files, null, options); defaultRoutes,
redirectRoutes,
rewriteRoutes,
errorRoutes,
limitedRoutes,
} = await detectBuilders(files, null, options);
testHeaders(redirectRoutes); testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]); expect(defaultRoutes).toStrictEqual([]);
expect(rewriteRoutes).toStrictEqual([ expect(rewriteRoutes).toStrictEqual([
@@ -2834,6 +2852,28 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
}, },
]); ]);
// Limited routes should have js but not go since the go plugin is not installed
expect(limitedRoutes).toStrictEqual({
redirectRoutes: [
{
src: '^/(api(?:.+)?)/index(?:\\.(?:js))?/?$',
headers: {
Location: '/$1',
},
status: 308,
},
{
src: '^/api/(.+)(?:\\.(?:js))/?$',
headers: {
Location: '/api/$1',
},
status: 308,
},
],
rewriteRoutes: [],
defaultRoutes: [],
});
// expected redirect should match inputs // expected redirect should match inputs
const getLocation = createReplaceLocation(redirectRoutes); const getLocation = createReplaceLocation(redirectRoutes);
@@ -3077,7 +3117,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{ {
const files = ['api/user.go', 'api/team.js', 'api/package.json']; const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { defaultRoutes, redirectRoutes, rewriteRoutes } = const { defaultRoutes, redirectRoutes, rewriteRoutes, limitedRoutes } =
await detectBuilders(files, null, options); await detectBuilders(files, null, options);
testHeaders(redirectRoutes); testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([]); expect(defaultRoutes).toStrictEqual([]);
@@ -3088,6 +3128,28 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
}, },
]); ]);
// Limited routes should have js but not go since the go plugin is not installed
expect(limitedRoutes).toStrictEqual({
redirectRoutes: [
{
src: '^/(api(?:.+)?)/index(?:\\.(?:js))?/?$',
headers: {
Location: '/$1/',
},
status: 308,
},
{
src: '^/api/(.+)(?:\\.(?:js))/?$',
headers: {
Location: '/api/$1/',
},
status: 308,
},
],
rewriteRoutes: [],
defaultRoutes: [],
});
// expected redirect should match inputs // expected redirect should match inputs
const getLocation = createReplaceLocation(redirectRoutes); const getLocation = createReplaceLocation(redirectRoutes);

View File

@@ -1,6 +1,6 @@
import { join } from 'path'; import { join } from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { BuildOptions, createLambda } from '../src'; import { BuildOptions, createLambda, FileFsRef } from '../src';
import { convertRuntimeToPlugin } from '../src/convert-runtime-to-plugin'; import { convertRuntimeToPlugin } from '../src/convert-runtime-to-plugin';
async function fsToJson(dir: string, output: Record<string, any> = {}) { async function fsToJson(dir: string, output: Record<string, any> = {}) {
@@ -18,24 +18,43 @@ async function fsToJson(dir: string, output: Record<string, any> = {}) {
return output; return output;
} }
const workPath = join(__dirname, 'walk', 'python-api'); const invalidFuncWorkpath = join(
__dirname,
'convert-runtime',
'invalid-functions'
);
const pythonApiWorkpath = join(__dirname, 'convert-runtime', 'python-api');
describe('convert-runtime-to-plugin', () => { describe('convert-runtime-to-plugin', () => {
afterEach(async () => { afterEach(async () => {
await fs.remove(join(workPath, '.output')); await fs.remove(join(invalidFuncWorkpath, '.output'));
await fs.remove(join(pythonApiWorkpath, '.output'));
}); });
it('should create correct fileystem for python', async () => { it('should create correct fileystem for python', async () => {
const ext = '.py';
const workPath = pythonApiWorkpath;
const handlerName = 'vc__handler__python';
const handlerFileName = handlerName + ext;
const lambdaOptions = { const lambdaOptions = {
handler: 'index.handler', handler: `${handlerName}.vc_handler`,
runtime: 'python3.9', runtime: 'python3.9',
memory: 512, memory: 512,
maxDuration: 5, maxDuration: 5,
environment: {}, environment: {},
regions: ['sfo1'],
}; };
const buildRuntime = async (opts: BuildOptions) => { const buildRuntime = async (opts: BuildOptions) => {
const handlerPath = join(workPath, handlerFileName);
// This is the usual time at which a Legacy Runtime writes its Lambda launcher.
await fs.writeFile(handlerPath, '# handler');
opts.files[handlerFileName] = new FileFsRef({
fsPath: handlerPath,
});
const lambda = await createLambda({ const lambda = await createLambda({
files: opts.files, files: opts.files,
...lambdaOptions, ...lambdaOptions,
@@ -43,26 +62,24 @@ describe('convert-runtime-to-plugin', () => {
return { output: lambda }; return { output: lambda };
}; };
const lambdaFiles = await fsToJson(workPath); const packageName = 'vercel-plugin-python';
const vercelConfig = JSON.parse(lambdaFiles['vercel.json']); const build = await convertRuntimeToPlugin(buildRuntime, packageName, ext);
delete lambdaFiles['vercel.json'];
const build = await convertRuntimeToPlugin(buildRuntime, '.py');
await build({ vercelConfig, workPath }); await build({ workPath });
const output = await fsToJson(join(workPath, '.output')); const output = await fsToJson(join(workPath, '.output'));
expect(output).toMatchObject({ expect(output).toMatchObject({
'functions-manifest.json': expect.stringContaining('{'), 'functions-manifest.json': expect.stringContaining('{'),
'runtime-traced-files': lambdaFiles,
server: { server: {
pages: { pages: {
api: { api: {
'index.py': expect.stringContaining('index'), 'index.py': expect.stringContaining('handler'),
'index.py.nft.json': expect.stringContaining('{'), 'index.py.nft.json': expect.stringContaining('{'),
users: { users: {
'get.py': expect.stringContaining('get'), 'get.py': expect.stringContaining('handler'),
'get.py.nft.json': expect.stringContaining('{'), 'get.py.nft.json': expect.stringContaining('{'),
'post.py': expect.stringContaining('post'), 'post.py': expect.stringContaining('handler'),
'post.py.nft.json': expect.stringContaining('{'), 'post.py.nft.json': expect.stringContaining('{'),
}, },
}, },
@@ -72,42 +89,30 @@ describe('convert-runtime-to-plugin', () => {
const funcManifest = JSON.parse(output['functions-manifest.json']); const funcManifest = JSON.parse(output['functions-manifest.json']);
expect(funcManifest).toMatchObject({ expect(funcManifest).toMatchObject({
version: 1, version: 2,
pages: { pages: {
'api/index.py': lambdaOptions, 'api/index.py': { ...lambdaOptions, handler: 'index.vc_handler' },
'api/users/get.py': lambdaOptions, 'api/users/get.py': { ...lambdaOptions, handler: 'get.vc_handler' },
'api/users/post.py': { ...lambdaOptions, memory: 3008 }, 'api/users/post.py': {
...lambdaOptions,
handler: 'post.vc_handler',
memory: 512,
},
}, },
}); });
const indexJson = JSON.parse(output.server.pages.api['index.py.nft.json']); const indexJson = JSON.parse(output.server.pages.api['index.py.nft.json']);
expect(indexJson).toMatchObject({ expect(indexJson).toMatchObject({
version: 1, version: 2,
files: [ files: [
{ '../../../../api/db/[id].py',
input: '../../../../runtime-traced-files/api/index.py', '../../../../api/index.py',
output: 'api/index.py', '../../../../api/project/[aid]/[bid]/index.py',
}, '../../../../api/users/get.py',
{ '../../../../api/users/post.py',
input: '../../../../runtime-traced-files/api/users/get.py', '../../../../file.txt',
output: 'api/users/get.py', '../../../../util/date.py',
}, '../../../../util/math.py',
{
input: '../../../../runtime-traced-files/api/users/post.py',
output: 'api/users/post.py',
},
{
input: '../../../../runtime-traced-files/file.txt',
output: 'file.txt',
},
{
input: '../../../../runtime-traced-files/util/date.py',
output: 'util/date.py',
},
{
input: '../../../../runtime-traced-files/util/math.py',
output: 'util/math.py',
},
], ],
}); });
@@ -115,32 +120,16 @@ describe('convert-runtime-to-plugin', () => {
output.server.pages.api.users['get.py.nft.json'] output.server.pages.api.users['get.py.nft.json']
); );
expect(getJson).toMatchObject({ expect(getJson).toMatchObject({
version: 1, version: 2,
files: [ files: [
{ '../../../../../api/db/[id].py',
input: '../../../../../runtime-traced-files/api/index.py', '../../../../../api/index.py',
output: 'api/index.py', '../../../../../api/project/[aid]/[bid]/index.py',
}, '../../../../../api/users/get.py',
{ '../../../../../api/users/post.py',
input: '../../../../../runtime-traced-files/api/users/get.py', '../../../../../file.txt',
output: 'api/users/get.py', '../../../../../util/date.py',
}, '../../../../../util/math.py',
{
input: '../../../../../runtime-traced-files/api/users/post.py',
output: 'api/users/post.py',
},
{
input: '../../../../../runtime-traced-files/file.txt',
output: 'file.txt',
},
{
input: '../../../../../runtime-traced-files/util/date.py',
output: 'util/date.py',
},
{
input: '../../../../../runtime-traced-files/util/math.py',
output: 'util/math.py',
},
], ],
}); });
@@ -148,32 +137,16 @@ describe('convert-runtime-to-plugin', () => {
output.server.pages.api.users['post.py.nft.json'] output.server.pages.api.users['post.py.nft.json']
); );
expect(postJson).toMatchObject({ expect(postJson).toMatchObject({
version: 1, version: 2,
files: [ files: [
{ '../../../../../api/db/[id].py',
input: '../../../../../runtime-traced-files/api/index.py', '../../../../../api/index.py',
output: 'api/index.py', '../../../../../api/project/[aid]/[bid]/index.py',
}, '../../../../../api/users/get.py',
{ '../../../../../api/users/post.py',
input: '../../../../../runtime-traced-files/api/users/get.py', '../../../../../file.txt',
output: 'api/users/get.py', '../../../../../util/date.py',
}, '../../../../../util/math.py',
{
input: '../../../../../runtime-traced-files/api/users/post.py',
output: 'api/users/post.py',
},
{
input: '../../../../../runtime-traced-files/file.txt',
output: 'file.txt',
},
{
input: '../../../../../runtime-traced-files/util/date.py',
output: 'util/date.py',
},
{
input: '../../../../../runtime-traced-files/util/math.py',
output: 'util/math.py',
},
], ],
}); });

View File

@@ -34,7 +34,7 @@ Finally, [connect your Git repository to Vercel](https://vercel.com/docs/git) an
## Documentation ## Documentation
For details on how to use Vercel CLI, check out our [documentation](https://vercel.com/docs). For details on how to use Vercel CLI, check out our [documentation](https://vercel.com/docs/cli).
## Local Development ## Local Development

View File

@@ -1,6 +1,6 @@
{ {
"name": "vercel", "name": "vercel",
"version": "23.1.3-canary.39", "version": "23.1.3-canary.67",
"preferGlobal": true, "preferGlobal": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "The command-line interface for Vercel", "description": "The command-line interface for Vercel",
@@ -43,14 +43,14 @@
"node": ">= 12" "node": ">= 12"
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "2.12.3-canary.21", "@vercel/build-utils": "2.12.3-canary.42",
"@vercel/go": "1.2.4-canary.4", "@vercel/go": "1.2.4-canary.4",
"@vercel/node": "1.12.2-canary.7", "@vercel/node": "1.12.2-canary.7",
"@vercel/python": "2.1.2-canary.0", "@vercel/python": "2.1.2-canary.2",
"@vercel/ruby": "1.2.8-canary.4", "@vercel/ruby": "1.2.10-canary.0",
"update-notifier": "4.1.0", "update-notifier": "4.1.0",
"vercel-plugin-middleware": "0.0.0-canary.7", "vercel-plugin-middleware": "0.0.0-canary.19",
"vercel-plugin-node": "1.12.2-canary.12" "vercel-plugin-node": "1.12.2-canary.34"
}, },
"devDependencies": { "devDependencies": {
"@next/env": "11.1.2", "@next/env": "11.1.2",
@@ -90,7 +90,7 @@
"@types/update-notifier": "5.1.0", "@types/update-notifier": "5.1.0",
"@types/which": "1.3.2", "@types/which": "1.3.2",
"@types/write-json-file": "2.2.1", "@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.5.1-canary.13", "@vercel/frameworks": "0.5.1-canary.17",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"@vercel/nft": "0.17.0", "@vercel/nft": "0.17.0",
"@zeit/fun": "0.11.2", "@zeit/fun": "0.11.2",

View File

@@ -5,16 +5,16 @@ import {
GlobOptions, GlobOptions,
scanParentDirs, scanParentDirs,
spawnAsync, spawnAsync,
glob as buildUtilsGlob,
} from '@vercel/build-utils'; } from '@vercel/build-utils';
import { nodeFileTrace } from '@vercel/nft'; import { nodeFileTrace } from '@vercel/nft';
import Sema from 'async-sema'; import Sema from 'async-sema';
import chalk from 'chalk'; import chalk from 'chalk';
import { SpawnOptions } from 'child_process'; import { SpawnOptions } from 'child_process';
import { assert } from 'console'; import { assert } from 'console';
import { createHash } from 'crypto';
import fs from 'fs-extra'; import fs from 'fs-extra';
import ogGlob from 'glob'; import ogGlob from 'glob';
import { isAbsolute, join, parse, relative, resolve } from 'path'; import { dirname, isAbsolute, join, parse, relative } from 'path';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import Client from '../util/client'; import Client from '../util/client';
import { VercelConfig } from '../util/dev/types'; import { VercelConfig } from '../util/dev/types';
@@ -136,9 +136,11 @@ export default async function main(client: Client) {
}); });
// Set process.env with loaded environment variables // Set process.env with loaded environment variables
await processEnv(loadedEnvFiles); processEnv(loadedEnvFiles);
const spawnOpts = { const spawnOpts: {
env: Record<string, string | undefined>;
} = {
env: { ...combinedEnv, VERCEL: '1' }, env: { ...combinedEnv, VERCEL: '1' },
}; };
@@ -284,6 +286,21 @@ export default async function main(client: Client) {
// Clean the output directory // Clean the output directory
fs.removeSync(join(cwd, OUTPUT_DIR)); fs.removeSync(join(cwd, OUTPUT_DIR));
if (framework && process.env.VERCEL_URL && 'envPrefix' in framework) {
for (const key of Object.keys(process.env)) {
if (key.startsWith('VERCEL_')) {
const newKey = `${framework.envPrefix}${key}`;
// Set `process.env` and `spawnOpts.env` to make sure the variables are
// available to the `build` step and the CLI Plugins.
process.env[newKey] = process.env[newKey] || process.env[key];
spawnOpts.env[newKey] = process.env[newKey];
}
}
}
// Required for Next.js to produce the correct `.nft.json` files.
spawnOpts.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = baseDir;
// Yarn v2 PnP mode may be activated, so force // Yarn v2 PnP mode may be activated, so force
// "node-modules" linker style // "node-modules" linker style
const env = { const env = {
@@ -315,22 +332,48 @@ export default async function main(client: Client) {
cwd, cwd,
}); });
} }
// don't trust framework detection here because they might be switching to next on a branch
const isNextJs = fs.existsSync(join(cwd, '.next'));
if (!fs.existsSync(join(cwd, OUTPUT_DIR))) { if (!fs.existsSync(join(cwd, OUTPUT_DIR))) {
let outputDir = join(OUTPUT_DIR, 'static'); let dotNextDir: string | null = null;
let distDir = await framework.getFsOutputDir(cwd);
if (isNextJs) { // If a custom `outputDirectory` was set, we'll need to verify
outputDir = OUTPUT_DIR; // if it's `.next` output, or just static output.
const userOutputDirectory = project.settings.outputDirectory;
if (typeof userOutputDirectory === 'string') {
if (fs.existsSync(join(cwd, userOutputDirectory, 'BUILD_ID'))) {
dotNextDir = join(cwd, userOutputDirectory);
client.output.debug(
`Consider ${param(userOutputDirectory)} as ${param('.next')} output.`
);
}
} else if (fs.existsSync(join(cwd, '.next'))) {
dotNextDir = join(cwd, '.next');
client.output.debug(`Found ${param('.next')} directory.`);
} }
const copyStamp = stamp();
// We cannot rely on the `framework` alone, as it might be a static export,
// and the current build might use a different project that's not in the settings.
const isNextOutput = Boolean(dotNextDir);
const nextExport = await getNextExportStatus(dotNextDir);
const outputDir =
isNextOutput && !nextExport ? OUTPUT_DIR : join(OUTPUT_DIR, 'static');
const getDistDir = framework.getFsOutputDir || framework.getOutputDirName;
const distDir =
(nextExport?.exportDetail.outDirectory
? relative(cwd, nextExport.exportDetail.outDirectory)
: false) ||
dotNextDir ||
userOutputDirectory ||
(await getDistDir(cwd));
await fs.ensureDir(join(cwd, outputDir)); await fs.ensureDir(join(cwd, outputDir));
const relativeDistDir = relative(cwd, distDir);
const copyStamp = stamp();
client.output.spinner( client.output.spinner(
`Copying files from ${param(distDir)} to ${param(outputDir)}` `Copying files from ${param(distDir)} to ${param(outputDir)}`
); );
const files = await glob(join(relativeDistDir, '**'), { const files = await glob(join(relative(cwd, distDir), '**'), {
ignore: [ ignore: [
'node_modules/**', 'node_modules/**',
'.vercel/**', '.vercel/**',
@@ -378,6 +421,7 @@ export default async function main(client: Client) {
`Generating build manifest: ${param(buildManifestPath)}` `Generating build manifest: ${param(buildManifestPath)}`
); );
const buildManifest = { const buildManifest = {
version: 1,
cache: framework.cachePattern ? [framework.cachePattern] : [], cache: framework.cachePattern ? [framework.cachePattern] : [],
}; };
await fs.writeJSON(buildManifestPath, buildManifest, { spaces: 2 }); await fs.writeJSON(buildManifestPath, buildManifest, { spaces: 2 });
@@ -405,7 +449,53 @@ export default async function main(client: Client) {
} }
// Special Next.js processing. // Special Next.js processing.
if (isNextJs) { if (nextExport) {
client.output.debug('Found `next export` output.');
const htmlFiles = await buildUtilsGlob(
'**/*.html',
join(cwd, OUTPUT_DIR, 'static')
);
if (nextExport.exportDetail.success !== true) {
client.output.error(
`Export of Next.js app failed. Please check your build logs.`
);
process.exit(1);
}
await fs.mkdirp(join(cwd, OUTPUT_DIR, 'server', 'pages'));
await fs.mkdirp(join(cwd, OUTPUT_DIR, 'static'));
await Promise.all(
Object.keys(htmlFiles).map(async fileName => {
await sema.acquire();
const input = join(cwd, OUTPUT_DIR, 'static', fileName);
const target = join(cwd, OUTPUT_DIR, 'server', 'pages', fileName);
await fs.mkdirp(dirname(target));
await fs.promises.rename(input, target).finally(() => {
sema.release();
});
})
);
for (const file of [
'BUILD_ID',
'images-manifest.json',
'routes-manifest.json',
'build-manifest.json',
]) {
const input = join(nextExport.dotNextDir, file);
if (fs.existsSync(input)) {
// Do not use `smartCopy`, since we want to overwrite if they already exist.
await fs.copyFile(input, join(OUTPUT_DIR, file));
}
}
} else if (isNextOutput) {
// The contents of `.output/static` should be placed inside of `.output/static/_next/static` // The contents of `.output/static` should be placed inside of `.output/static/_next/static`
const tempStatic = '___static'; const tempStatic = '___static';
await fs.rename( await fs.rename(
@@ -456,10 +546,12 @@ export default async function main(client: Client) {
// `public`, then`static`). We can't read both at the same time because that would mean we'd // `public`, then`static`). We can't read both at the same time because that would mean we'd
// read public for old Next.js versions that don't support it, which might be breaking (and // read public for old Next.js versions that don't support it, which might be breaking (and
// we don't want to make vercel build specific framework versions). // we don't want to make vercel build specific framework versions).
const nextSrcDirectory = dirname(distDir);
const publicFiles = await glob('public/**', { const publicFiles = await glob('public/**', {
nodir: true, nodir: true,
dot: true, dot: true,
cwd, cwd: nextSrcDirectory,
absolute: true, absolute: true,
}); });
if (publicFiles.length > 0) { if (publicFiles.length > 0) {
@@ -468,7 +560,11 @@ export default async function main(client: Client) {
smartCopy( smartCopy(
client, client,
f, f,
f.replace('public', join(OUTPUT_DIR, 'static')) join(
OUTPUT_DIR,
'static',
relative(join(dirname(distDir), 'public'), f)
)
) )
) )
); );
@@ -476,7 +572,7 @@ export default async function main(client: Client) {
const staticFiles = await glob('static/**', { const staticFiles = await glob('static/**', {
nodir: true, nodir: true,
dot: true, dot: true,
cwd, cwd: nextSrcDirectory,
absolute: true, absolute: true,
}); });
await Promise.all( await Promise.all(
@@ -484,7 +580,12 @@ export default async function main(client: Client) {
smartCopy( smartCopy(
client, client,
f, f,
f.replace('static', join(OUTPUT_DIR, 'static', 'static')) join(
OUTPUT_DIR,
'static',
'static',
relative(join(dirname(distDir), 'static'), f)
)
) )
) )
); );
@@ -503,6 +604,7 @@ export default async function main(client: Client) {
const nftFiles = await glob(join(OUTPUT_DIR, '**', '*.nft.json'), { const nftFiles = await glob(join(OUTPUT_DIR, '**', '*.nft.json'), {
nodir: true, nodir: true,
dot: true, dot: true,
ignore: ['cache/**'],
cwd, cwd,
absolute: true, absolute: true,
}); });
@@ -534,28 +636,15 @@ export default async function main(client: Client) {
], ],
}); });
fileList.delete(relative(cwd, f)); fileList.delete(relative(cwd, f));
await resolveNftToOutput({
client, const nftFileName = f.replace(ext, '.js.nft.json');
baseDir, client.output.debug(`Creating ${nftFileName}`);
outputDir: OUTPUT_DIR,
nftFileName: f.replace(ext, '.js.nft.json'), await fs.writeJSON(nftFileName, {
nft: { version: 2,
version: 1, files: Array.from(fileList).map(fileListEntry =>
files: Array.from(fileList).map(fileListEntry => relative(dir, fileListEntry)
relative(dir, fileListEntry) ),
),
},
});
}
} else {
for (let f of nftFiles) {
const json = await fs.readJson(f);
await resolveNftToOutput({
client,
baseDir,
outputDir: OUTPUT_DIR,
nftFileName: f,
nft: json,
}); });
} }
} }
@@ -564,22 +653,25 @@ export default async function main(client: Client) {
OUTPUT_DIR, OUTPUT_DIR,
'required-server-files.json' 'required-server-files.json'
); );
const requiredServerFilesJson = await fs.readJSON(
requiredServerFilesPath
);
await fs.writeJSON(requiredServerFilesPath, {
...requiredServerFilesJson,
appDir: '.',
files: requiredServerFilesJson.files.map((i: string) => {
const absolutePath = join(cwd, i.replace('.next', '.output'));
const output = relative(baseDir, absolutePath);
return { if (fs.existsSync(requiredServerFilesPath)) {
input: i.replace('.next', '.output'), client.output.debug(`Resolve ${param('required-server-files.json')}.`);
output,
}; const requiredServerFilesJson = await fs.readJSON(
}), requiredServerFilesPath
}); );
await fs.writeJSON(requiredServerFilesPath, {
...requiredServerFilesJson,
appDir: '.',
files: requiredServerFilesJson.files.map((i: string) => {
const originalPath = join(requiredServerFilesJson.appDir, i);
const relPath = join(OUTPUT_DIR, relative(distDir, originalPath));
return relPath;
}),
});
}
} }
} }
@@ -734,70 +826,51 @@ async function glob(pattern: string, options: GlobOptions): Promise<string[]> {
} }
/** /**
* Computes a hash for the given buf. * Files will only exist when `next export` was used.
*
* @param {Buffer} file data
* @return {String} hex digest
*/ */
function hash(buf: Buffer): string { async function getNextExportStatus(dotNextDir: string | null) {
return createHash('sha1').update(buf).digest('hex'); if (!dotNextDir) {
} return null;
interface NftFile {
version: number;
files: (string | { input: string; output: string })[];
}
// resolveNftToOutput takes nft file and moves all of its trace files
// into the specified directory + `inputs`, (renaming them to their hash + ext) and
// subsequently updating the original nft file accordingly. This is done
// to make the `.output` directory be self-contained, so that it works
// properly with `vc --prebuilt`.
async function resolveNftToOutput({
client,
baseDir,
outputDir,
nftFileName,
nft,
}: {
client: Client;
baseDir: string;
outputDir: string;
nftFileName: string;
nft: NftFile;
}) {
client.output.debug(`Processing and resolving ${nftFileName}`);
await fs.ensureDir(join(outputDir, 'inputs'));
const newFilesList: NftFile['files'] = [];
for (let fileEntity of nft.files) {
const relativeInput: string =
typeof fileEntity === 'string' ? fileEntity : fileEntity.input;
const fullInput = resolve(join(parse(nftFileName).dir, relativeInput));
// if the resolved path is NOT in the .output directory we move in it there
if (!fullInput.includes(outputDir)) {
const { ext } = parse(fullInput);
const raw = await fs.readFile(fullInput);
const newFilePath = join(outputDir, 'inputs', hash(raw) + ext);
smartCopy(client, fullInput, newFilePath);
// We have to use `baseDir` instead of `cwd`, because we want to
// mount everything from there (especially `node_modules`).
// This is important for NPM Workspaces where `node_modules` is not
// in the directory of the workspace.
const output = relative(baseDir, fullInput).replace('.output', '.next');
newFilesList.push({
input: relative(parse(nftFileName).dir, newFilePath),
output,
});
} else {
newFilesList.push(relativeInput);
}
} }
// Update the .nft.json with new input and output mapping
await fs.writeJSON(nftFileName, { const exportDetail: {
...nft, success: boolean;
files: newFilesList, outDirectory: string;
}); } | null = await fs
.readJson(join(dotNextDir, 'export-detail.json'))
.catch(error => {
if (error.code === 'ENOENT') {
return null;
}
throw error;
});
if (!exportDetail) {
return null;
}
const exportMarker: {
version: 1;
exportTrailingSlash: boolean;
hasExportPathMap: boolean;
} | null = await fs
.readJSON(join(dotNextDir, 'export-marker.json'))
.catch(error => {
if (error.code === 'ENOENT') {
return null;
}
throw error;
});
return {
dotNextDir,
exportDetail,
exportMarker: {
trailingSlash: exportMarker?.hasExportPathMap
? exportMarker.exportTrailingSlash
: false,
},
};
} }

View File

@@ -447,6 +447,7 @@ export default async (client: Client) => {
forceNew: argv['--force'], forceNew: argv['--force'],
withCache: argv['--with-cache'], withCache: argv['--with-cache'],
prebuilt: argv['--prebuilt'], prebuilt: argv['--prebuilt'],
rootDirectory,
quiet, quiet,
wantsPublic: argv['--public'] || localConfig.public, wantsPublic: argv['--public'] || localConfig.public,
isFile, isFile,

View File

@@ -52,6 +52,7 @@ export default async function processDeployment({
isSettingUpProject: boolean; isSettingUpProject: boolean;
skipAutoDetectionConfirmation?: boolean; skipAutoDetectionConfirmation?: boolean;
cwd?: string; cwd?: string;
rootDirectory?: string;
}) { }) {
let { let {
now, now,
@@ -64,6 +65,7 @@ export default async function processDeployment({
nowConfig, nowConfig,
quiet, quiet,
prebuilt, prebuilt,
rootDirectory,
} = args; } = args;
const { debug } = output; const { debug } = output;
@@ -86,6 +88,7 @@ export default async function processDeployment({
force, force,
withCache, withCache,
prebuilt, prebuilt,
rootDirectory,
skipAutoDetectionConfirmation, skipAutoDetectionConfirmation,
}; };

View File

@@ -968,7 +968,7 @@ export default class DevServer {
socket.destroy(); socket.destroy();
return; return;
} }
const target = `http://localhost:${this.devProcessPort}`; const target = `http://127.0.0.1:${this.devProcessPort}`;
this.output.debug(`Detected "upgrade" event, proxying to ${target}`); this.output.debug(`Detected "upgrade" event, proxying to ${target}`);
this.proxy.ws(req, socket, head, { target }); this.proxy.ws(req, socket, head, { target });
}); });
@@ -1663,7 +1663,7 @@ export default class DevServer {
if (!match) { if (!match) {
// If the dev command is started, then proxy to it // If the dev command is started, then proxy to it
if (this.devProcessPort) { if (this.devProcessPort) {
const upstream = `http://localhost:${this.devProcessPort}`; const upstream = `http://127.0.0.1:${this.devProcessPort}`;
debug(`Proxying to frontend dev server: ${upstream}`); debug(`Proxying to frontend dev server: ${upstream}`);
// Add the Vercel platform proxy request headers // Add the Vercel platform proxy request headers
@@ -1810,7 +1810,7 @@ export default class DevServer {
return proxyPass( return proxyPass(
req, req,
res, res,
`http://localhost:${port}`, `http://127.0.0.1:${port}`,
this, this,
requestId, requestId,
false false
@@ -1847,7 +1847,7 @@ export default class DevServer {
return proxyPass( return proxyPass(
req, req,
res, res,
`http://localhost:${this.devProcessPort}`, `http://127.0.0.1:${this.devProcessPort}`,
this, this,
requestId, requestId,
false false

View File

@@ -37,6 +37,7 @@ export interface CreateOptions {
project?: string; project?: string;
wantsPublic: boolean; wantsPublic: boolean;
prebuilt?: boolean; prebuilt?: boolean;
rootDirectory?: string;
meta: Dictionary<string>; meta: Dictionary<string>;
regions?: string[]; regions?: string[];
quiet?: boolean; quiet?: boolean;
@@ -113,6 +114,7 @@ export default class Now extends EventEmitter {
name, name,
project, project,
prebuilt = false, prebuilt = false,
rootDirectory,
wantsPublic, wantsPublic,
meta, meta,
regions, regions,
@@ -168,6 +170,7 @@ export default class Now extends EventEmitter {
skipAutoDetectionConfirmation, skipAutoDetectionConfirmation,
cwd, cwd,
prebuilt, prebuilt,
rootDirectory,
}); });
if (deployment && deployment.warnings) { if (deployment && deployment.warnings) {

View File

@@ -8,6 +8,7 @@ export type ProjectLinkAndSettings = ProjectLink & {
buildCommand: Project['buildCommand']; buildCommand: Project['buildCommand'];
devCommand: Project['devCommand']; devCommand: Project['devCommand'];
outputDirectory: Project['outputDirectory']; outputDirectory: Project['outputDirectory'];
directoryListing: Project['directoryListing'];
rootDirectory: Project['rootDirectory']; rootDirectory: Project['rootDirectory'];
framework: Project['framework']; framework: Project['framework'];
}; };
@@ -29,6 +30,7 @@ export async function writeProjectSettings(
settings: { settings: {
buildCommand: project.buildCommand, buildCommand: project.buildCommand,
devCommand: project.devCommand, devCommand: project.devCommand,
outputDirectory: project.outputDirectory,
directoryListing: project.directoryListing, directoryListing: project.directoryListing,
rootDirectory: project.rootDirectory, rootDirectory: project.rootDirectory,
framework: project.framework, framework: project.framework,

View File

@@ -0,0 +1,6 @@
export default function (req) {
const isStrict = (function () {
return !this;
})();
return new Response('is strict mode? ' + (isStrict ? 'yes' : 'no'));
}

View File

@@ -385,4 +385,13 @@ describe('DevServer', () => {
); );
}) })
); );
it(
'should run middleware in strict mode',
testFixture('edge-middleware-strict', async server => {
const response = await fetch(`${server.address}/index.html`);
const body = await response.text();
expect(body).toStrictEqual('is strict mode? yes');
})
);
}); });

View File

@@ -6,3 +6,5 @@ node_modules
!tests/fixtures/nowignore/node_modules !tests/fixtures/nowignore/node_modules
!tests/fixtures/vercelignore-allow-nodemodules/node_modules !tests/fixtures/vercelignore-allow-nodemodules/node_modules
!tests/fixtures/vercelignore-allow-nodemodules/sub/node_modules !tests/fixtures/vercelignore-allow-nodemodules/sub/node_modules
!tests/fixtures/file-system-api/.output
!tests/fixtures/file-system-api-root-directory/**/.output

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/client", "name": "@vercel/client",
"version": "10.2.3-canary.22", "version": "10.2.3-canary.45",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"homepage": "https://vercel.com", "homepage": "https://vercel.com",
@@ -40,7 +40,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "2.12.3-canary.21", "@vercel/build-utils": "2.12.3-canary.42",
"@zeit/fetch": "5.2.0", "@zeit/fetch": "5.2.0",
"async-retry": "1.2.3", "async-retry": "1.2.3",
"async-sema": "3.0.0", "async-sema": "3.0.0",

View File

@@ -1,12 +1,12 @@
import { lstatSync } from 'fs-extra'; import { lstatSync } from 'fs-extra';
import { relative, isAbsolute } from 'path'; import { relative, isAbsolute } from 'path';
import hashes, { mapToObject } from './utils/hashes'; import { hashes, mapToObject, resolveNftJsonFiles } from './utils/hashes';
import { upload } from './upload'; import { upload } from './upload';
import { buildFileTree, createDebug, parseVercelConfig } from './utils'; import { buildFileTree, createDebug, parseVercelConfig } from './utils';
import { DeploymentError } from './errors'; import { DeploymentError } from './errors';
import { import {
NowConfig, VercelConfig,
VercelClientOptions, VercelClientOptions,
DeploymentOptions, DeploymentOptions,
DeploymentEventType, DeploymentEventType,
@@ -16,7 +16,7 @@ export default function buildCreateDeployment() {
return async function* createDeployment( return async function* createDeployment(
clientOptions: VercelClientOptions, clientOptions: VercelClientOptions,
deploymentOptions: DeploymentOptions = {}, deploymentOptions: DeploymentOptions = {},
nowConfig: NowConfig = {} nowConfig: VercelConfig = {}
): AsyncIterableIterator<{ type: DeploymentEventType; payload: any }> { ): AsyncIterableIterator<{ type: DeploymentEventType; payload: any }> {
const { path } = clientOptions; const { path } = clientOptions;
@@ -74,12 +74,7 @@ export default function buildCreateDeployment() {
debug(`Provided 'path' is a single file`); debug(`Provided 'path' is a single file`);
} }
let { fileList } = await buildFileTree( let { fileList } = await buildFileTree(path, clientOptions, debug);
path,
clientOptions.isDirectory,
debug,
clientOptions.prebuilt
);
let configPath: string | undefined; let configPath: string | undefined;
if (!nowConfig) { if (!nowConfig) {
@@ -114,7 +109,11 @@ export default function buildCreateDeployment() {
}; };
} }
const files = await hashes(fileList); const hashedFileMap = await hashes(fileList);
const nftFileList = clientOptions.prebuilt
? await resolveNftJsonFiles(hashedFileMap)
: [];
const files = await hashes(nftFileList, hashedFileMap);
debug(`Yielding a 'hashes-calculated' event with ${files.size} hashes`); debug(`Yielding a 'hashes-calculated' event with ${files.size} hashes`);
yield { type: 'hashes-calculated', payload: mapToObject(files) }; yield { type: 'hashes-calculated', payload: mapToObject(files) };

View File

@@ -15,6 +15,7 @@ export interface VercelClientOptions {
apiUrl?: string; apiUrl?: string;
force?: boolean; force?: boolean;
prebuilt?: boolean; prebuilt?: boolean;
rootDirectory?: string;
withCache?: boolean; withCache?: boolean;
userAgent?: string; userAgent?: string;
defaultName?: string; defaultName?: string;

View File

@@ -1,6 +1,7 @@
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { Sema } from 'async-sema'; import { Sema } from 'async-sema';
import { join, dirname } from 'path';
export interface DeploymentFile { export interface DeploymentFile {
names: string[]; names: string[];
@@ -15,9 +16,7 @@ export interface DeploymentFile {
* @return {String} hex digest * @return {String} hex digest
*/ */
function hash(buf: Buffer): string { function hash(buf: Buffer): string {
return createHash('sha1') return createHash('sha1').update(buf).digest('hex');
.update(buf)
.digest('hex');
} }
/** /**
@@ -39,34 +38,68 @@ export const mapToObject = (
/** /**
* Computes hashes for the contents of each file given. * Computes hashes for the contents of each file given.
* *
* @param {Array} of {String} full paths * @param files - absolute file paths
* @return {Map} * @param map - optional map of files to append
* @return Map of hash digest to file object
*/ */
async function hashes(files: string[]): Promise<Map<string, DeploymentFile>> { export async function hashes(
const map = new Map<string, DeploymentFile>(); files: string[],
map = new Map<string, DeploymentFile>()
): Promise<Map<string, DeploymentFile>> {
const semaphore = new Sema(100); const semaphore = new Sema(100);
await Promise.all( await Promise.all(
files.map( files.map(async (name: string): Promise<void> => {
async (name: string): Promise<void> => { await semaphore.acquire();
await semaphore.acquire(); const data = await fs.readFile(name);
const data = await fs.readFile(name); const { mode } = await fs.stat(name);
const { mode } = await fs.stat(name);
const h = hash(data); const h = hash(data);
const entry = map.get(h); const entry = map.get(h);
if (entry) { if (entry) {
if (entry.names[0] !== name) {
entry.names.push(name); entry.names.push(name);
} else {
map.set(h, { names: [name], data, mode });
} }
} else {
semaphore.release(); map.set(h, { names: [name], data, mode });
} }
)
semaphore.release();
})
); );
return map; return map;
} }
export default hashes; export async function resolveNftJsonFiles(
hashedFiles: Map<string, DeploymentFile>
): Promise<string[]> {
const semaphore = new Sema(100);
const existingFiles = Array.from(hashedFiles.values());
const resolvedFiles = new Set<string>();
await Promise.all(
existingFiles.map(async file => {
await semaphore.acquire();
const fsPath = file.names[0];
if (fsPath.endsWith('.nft.json')) {
const json = file.data.toString('utf8');
const { version, files } = JSON.parse(json) as {
version: number;
files: string[] | { input: string; output: string }[];
};
if (version === 1 || version === 2) {
for (let f of files) {
const relPath = typeof f === 'string' ? f : f.input;
resolvedFiles.add(join(dirname(fsPath), relPath));
}
} else {
console.error(`Invalid nft.json version: ${version}`);
}
}
semaphore.release();
})
);
return Array.from(resolvedFiles);
}

View File

@@ -1,7 +1,7 @@
import { DeploymentFile } from './hashes'; import { DeploymentFile } from './hashes';
import { FetchOptions } from '@zeit/fetch'; import { FetchOptions } from '@zeit/fetch';
import { nodeFetch, zeitFetch } from './fetch'; import { nodeFetch, zeitFetch } from './fetch';
import { join, sep, relative } from 'path'; import { join, sep, relative, posix } from 'path';
import { URL } from 'url'; import { URL } from 'url';
import ignore from 'ignore'; import ignore from 'ignore';
type Ignore = ReturnType<typeof ignore>; type Ignore = ReturnType<typeof ignore>;
@@ -81,13 +81,16 @@ const maybeRead = async function <T>(path: string, default_: T) {
export async function buildFileTree( export async function buildFileTree(
path: string | string[], path: string | string[],
isDirectory: boolean, {
debug: Debug, isDirectory,
prebuilt?: boolean prebuilt,
rootDirectory,
}: Pick<VercelClientOptions, 'isDirectory' | 'prebuilt' | 'rootDirectory'>,
debug: Debug
): Promise<{ fileList: string[]; ignoreList: string[] }> { ): Promise<{ fileList: string[]; ignoreList: string[] }> {
const ignoreList: string[] = []; const ignoreList: string[] = [];
let fileList: string[]; let fileList: string[];
let { ig, ignores } = await getVercelIgnore(path, prebuilt); let { ig, ignores } = await getVercelIgnore(path, prebuilt, rootDirectory);
debug(`Found ${ignores.length} rules in .vercelignore`); debug(`Found ${ignores.length} rules in .vercelignore`);
debug('Building file tree...'); debug('Building file tree...');
@@ -119,37 +122,50 @@ export async function buildFileTree(
export async function getVercelIgnore( export async function getVercelIgnore(
cwd: string | string[], cwd: string | string[],
prebuilt?: boolean prebuilt?: boolean,
rootDirectory?: string
): Promise<{ ig: Ignore; ignores: string[] }> { ): Promise<{ ig: Ignore; ignores: string[] }> {
const ignores: string[] = prebuilt let ignores: string[] = [];
? ['*', '!.output', '!.output/**']
: [ const outputDir = posix.join(rootDirectory || '', '.output');
'.hg',
'.git', if (prebuilt) {
'.gitmodules', ignores.push('*');
'.svn', const parts = outputDir.split('/');
'.cache', parts.forEach((_, i) => {
'.next', const level = parts.slice(0, i + 1).join('/');
'.now', ignores.push(`!${level}`);
'.vercel', });
'.npmignore', ignores.push(`!${outputDir}/**`);
'.dockerignore', } else {
'.gitignore', ignores = [
'.*.swp', '.hg',
'.DS_Store', '.git',
'.wafpicke-*', '.gitmodules',
'.lock-wscript', '.svn',
'.env.local', '.cache',
'.env.*.local', '.next',
'.venv', '.now',
'npm-debug.log', '.vercel',
'config.gypi', '.npmignore',
'node_modules', '.dockerignore',
'__pycache__', '.gitignore',
'venv', '.*.swp',
'CVS', '.DS_Store',
'.output', '.wafpicke-*',
]; '.lock-wscript',
'.env.local',
'.env.*.local',
'.venv',
'npm-debug.log',
'config.gypi',
'node_modules',
'__pycache__',
'venv',
'CVS',
`.output`,
];
}
const cwds = Array.isArray(cwd) ? cwd : [cwd]; const cwds = Array.isArray(cwd) ? cwd : [cwd];
const files = await Promise.all( const files = await Promise.all(
@@ -250,39 +266,31 @@ export const prepareFiles = (
files: Map<string, DeploymentFile>, files: Map<string, DeploymentFile>,
clientOptions: VercelClientOptions clientOptions: VercelClientOptions
): PreparedFile[] => { ): PreparedFile[] => {
const preparedFiles = [...files.keys()].reduce( const preparedFiles: PreparedFile[] = [];
(acc: PreparedFile[], sha: string): PreparedFile[] => { for (const [sha, file] of files) {
const next = [...acc]; for (const name of file.names) {
let fileName: string;
const file = files.get(sha) as DeploymentFile; if (clientOptions.isDirectory) {
// Directory
for (const name of file.names) { fileName =
let fileName: string; typeof clientOptions.path === 'string'
? relative(clientOptions.path, name)
if (clientOptions.isDirectory) { : name;
// Directory } else {
fileName = // Array of files or single file
typeof clientOptions.path === 'string' const segments = name.split(sep);
? relative(clientOptions.path, name) fileName = segments[segments.length - 1];
: name;
} else {
// Array of files or single file
const segments = name.split(sep);
fileName = segments[segments.length - 1];
}
next.push({
file: isWin ? fileName.replace(/\\/g, '/') : fileName,
size: file.data.byteLength || file.data.length,
mode: file.mode,
sha,
});
} }
return next; preparedFiles.push({
}, file: isWin ? fileName.replace(/\\/g, '/') : fileName,
[] size: file.data.byteLength || file.data.length,
); mode: file.mode,
sha,
});
}
}
return preparedFiles; return preparedFiles;
}; };

View File

@@ -0,0 +1 @@
foo

View File

@@ -0,0 +1 @@
bar

View File

@@ -0,0 +1 @@
bar

View File

@@ -0,0 +1 @@
baz

View File

@@ -0,0 +1 @@
qux

View File

@@ -0,0 +1 @@
foo

View File

@@ -0,0 +1 @@
bar

View File

@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["*.test.ts"]
}

View File

@@ -17,7 +17,11 @@ const toAbsolutePaths = (cwd: string, files: string[]) =>
describe('buildFileTree()', () => { describe('buildFileTree()', () => {
it('should exclude files using `.nowignore` blocklist', async () => { it('should exclude files using `.nowignore` blocklist', async () => {
const cwd = fixture('nowignore'); const cwd = fixture('nowignore');
const { fileList, ignoreList } = await buildFileTree(cwd, true, noop); const { fileList, ignoreList } = await buildFileTree(
cwd,
{ isDirectory: true },
noop
);
const expectedFileList = toAbsolutePaths(cwd, ['.nowignore', 'index.txt']); const expectedFileList = toAbsolutePaths(cwd, ['.nowignore', 'index.txt']);
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual( expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
@@ -36,7 +40,11 @@ describe('buildFileTree()', () => {
it('should include the node_modules using `.vercelignore` allowlist', async () => { it('should include the node_modules using `.vercelignore` allowlist', async () => {
const cwd = fixture('vercelignore-allow-nodemodules'); const cwd = fixture('vercelignore-allow-nodemodules');
const { fileList, ignoreList } = await buildFileTree(cwd, true, noop); const { fileList, ignoreList } = await buildFileTree(
cwd,
{ isDirectory: true },
noop
);
const expected = toAbsolutePaths(cwd, [ const expected = toAbsolutePaths(cwd, [
'node_modules/one.txt', 'node_modules/one.txt',
@@ -54,4 +62,90 @@ describe('buildFileTree()', () => {
normalizeWindowsPaths(ignoreList).sort() normalizeWindowsPaths(ignoreList).sort()
); );
}); });
it('should find root files but ignore .output files when prebuilt=false', async () => {
const cwd = fixture('file-system-api');
const { fileList, ignoreList } = await buildFileTree(
cwd,
{ isDirectory: true, prebuilt: false },
noop
);
const expectedFileList = toAbsolutePaths(cwd, ['foo.txt', 'sub/bar.txt']);
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
normalizeWindowsPaths(fileList).sort()
);
const expectedIgnoreList = ['.output'];
expect(normalizeWindowsPaths(expectedIgnoreList).sort()).toEqual(
normalizeWindowsPaths(ignoreList).sort()
);
});
it('should find .output files but ignore other files when prebuilt=true', async () => {
const cwd = fixture('file-system-api');
const { fileList, ignoreList } = await buildFileTree(
cwd,
{ isDirectory: true, prebuilt: true },
noop
);
const expectedFileList = toAbsolutePaths(cwd, [
'.output/baz.txt',
'.output/sub/qux.txt',
]);
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
normalizeWindowsPaths(fileList).sort()
);
const expectedIgnoreList = ['foo.txt', 'sub'];
expect(normalizeWindowsPaths(expectedIgnoreList).sort()).toEqual(
normalizeWindowsPaths(ignoreList).sort()
);
});
it('should find root files but ignore all .output files when prebuilt=false and rootDirectory=root', async () => {
const cwd = fixture('file-system-api-root-directory');
const { fileList, ignoreList } = await buildFileTree(
cwd,
{ isDirectory: true, prebuilt: false, rootDirectory: 'root' },
noop
);
const expectedFileList = toAbsolutePaths(cwd, [
'foo.txt',
'root/bar.txt',
'someother/bar.txt',
]);
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
normalizeWindowsPaths(fileList).sort()
);
const expectedIgnoreList = ['root/.output', 'someother/.output'];
expect(normalizeWindowsPaths(expectedIgnoreList).sort()).toEqual(
normalizeWindowsPaths(ignoreList).sort()
);
});
it('should find root/.output files but ignore other files when prebuilt=true and rootDirectory=root', async () => {
const cwd = fixture('file-system-api-root-directory');
const { fileList, ignoreList } = await buildFileTree(
cwd,
{ isDirectory: true, prebuilt: true, rootDirectory: 'root' },
noop
);
const expectedFileList = toAbsolutePaths(cwd, [
'root/.output/baz.txt',
'root/.output/sub/qux.txt',
]);
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
normalizeWindowsPaths(fileList).sort()
);
const expectedIgnoreList = ['foo.txt', 'root/bar.txt', 'someother'];
expect(normalizeWindowsPaths(expectedIgnoreList).sort()).toEqual(
normalizeWindowsPaths(ignoreList).sort()
);
});
}); });

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M700 0H100C44.772 0 0 44.772 0 100v600c0 55.228 44.772 100 100 100h600c55.228 0 100-44.772 100-100V100C800 44.772 755.228 0 700 0Z" fill="#212121"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M587.947 527.768c4.254 54.65 4.254 80.268 4.254 108.232H465.756c0-6.091.109-11.663.219-17.313.342-17.564.699-35.88-2.147-72.868-3.761-54.152-27.08-66.185-69.957-66.185H195v-98.525h204.889c54.16 0 81.241-16.476 81.241-60.098 0-38.357-27.081-61.601-81.241-61.601H195V163h227.456C545.069 163 606 220.912 606 313.42c0 69.193-42.877 114.319-100.799 121.84 48.895 9.777 77.48 37.605 82.746 92.508Z" fill="#fff"/>
<path d="M195 636v-73.447h133.697c22.332 0 27.181 16.563 27.181 26.441V636H195Z" fill="#fff"/>
<path d="M194.5 636v.5h161.878v-47.506c0-5.006-1.226-11.734-5.315-17.224-4.108-5.515-11.059-9.717-22.366-9.717H194.5V636Z" stroke="#fff" stroke-opacity=".8"/>
</svg>

After

Width:  |  Height:  |  Size: 958 B

View File

@@ -1,25 +1,6 @@
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="800" fill="#212121"/> <path d="M700 0H100C44.772 0 0 44.772 0 100v600c0 55.228 44.772 100 100 100h600c55.228 0 100-44.772 100-100V100C800 44.772 755.228 0 700 0Z" fill="#212121"/>
<g filter="url(#filter0_dd_126_53)"> <path fill-rule="evenodd" clip-rule="evenodd" d="M587.947 527.768c4.254 54.65 4.254 80.268 4.254 108.232H465.756c0-6.091.109-11.663.219-17.313.342-17.564.699-35.88-2.147-72.868-3.761-54.152-27.08-66.185-69.957-66.185H195v-98.525h204.889c54.16 0 81.241-16.476 81.241-60.098 0-38.357-27.081-61.601-81.241-61.601H195V163h227.456C545.069 163 606 220.912 606 313.42c0 69.193-42.877 114.319-100.799 121.84 48.895 9.777 77.48 37.605 82.746 92.508Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M587.947 527.768C592.201 582.418 592.201 608.036 592.201 636H465.756C465.756 629.909 465.865 624.337 465.975 618.687C466.317 601.123 466.674 582.807 463.828 545.819C460.067 491.667 436.748 479.634 393.871 479.634H355.883H195V381.109H399.889C454.049 381.109 481.13 364.633 481.13 321.011C481.13 282.654 454.049 259.41 399.889 259.41H195V163H422.456C545.069 163 606 220.912 606 313.42C606 382.613 563.123 427.739 505.201 435.26C554.096 445.037 582.681 472.865 587.947 527.768Z" fill="#E8F2FF"/> <path d="M195 636v-73.447h133.697c22.332 0 27.181 16.563 27.181 26.441V636H195Z" fill="#fff"/>
<path d="M195 636V562.553H328.697C351.029 562.553 355.878 579.116 355.878 588.994V636H195Z" fill="#E8F2FF"/> <path d="M194.5 636v.5h161.878v-47.506c0-5.006-1.226-11.734-5.315-17.224-4.108-5.515-11.059-9.717-22.366-9.717H194.5V636Z" stroke="#fff" stroke-opacity=".8"/>
</g>
<defs>
<filter id="filter0_dd_126_53" x="131" y="99" width="539" height="601" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="28"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.223529 0 0 0 0 0.572549 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_126_53"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="32"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.223529 0 0 0 0 0.572549 0 0 0 0 1 0 0 0 0.9 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_126_53" result="effect2_dropShadow_126_53"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_126_53" result="shape"/>
</filter>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 958 B

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/frameworks", "name": "@vercel/frameworks",
"version": "0.5.1-canary.13", "version": "0.5.1-canary.17",
"main": "./dist/frameworks.js", "main": "./dist/frameworks.js",
"types": "./dist/frameworks.d.ts", "types": "./dist/frameworks.d.ts",
"files": [ "files": [

View File

@@ -141,7 +141,6 @@ export const frameworks = [
}, },
dependency: 'gatsby', dependency: 'gatsby',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
getFsOutputDir: async () => 'public',
defaultRoutes: async (dirPrefix: string) => { defaultRoutes: async (dirPrefix: string) => {
// This file could be generated by gatsby-plugin-now or gatsby-plugin-zeit-now // This file could be generated by gatsby-plugin-now or gatsby-plugin-zeit-now
try { try {
@@ -195,7 +194,7 @@ export const frameworks = [
name: 'Remix', name: 'Remix',
slug: 'remix', slug: 'remix',
demo: 'https://remix.examples.vercel.com', demo: 'https://remix.examples.vercel.com',
logo: 'https://raw.githubusercontent.com/vercel/vercel/main/packages/frameworks/logos/remix.svg', logo: 'https://raw.githubusercontent.com/vercel/vercel/main/packages/frameworks/logos/remix-no-shadow.svg',
tagline: 'Build Better Websites', tagline: 'Build Better Websites',
description: 'A new Remix app — the result of running `npx create-remix`.', description: 'A new Remix app — the result of running `npx create-remix`.',
website: 'https://remix.run', website: 'https://remix.run',
@@ -226,7 +225,6 @@ export const frameworks = [
}, },
}, },
dependency: 'remix', dependency: 'remix',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -251,13 +249,16 @@ export const frameworks = [
], ],
defaultHeaders: [ defaultHeaders: [
{ {
source: '^/build/(.*)$', source: '/build/(.*)',
regex: '^/build/(.*)$', regex: '/build/(.*)',
headers: [ headers: [
{ key: 'cache-control', value: 'public, max-age=31536000, immutable' }, {
key: 'cache-control',
value: 'public, max-age=31536000, immutable',
},
], ],
}, },
] ],
}, },
{ {
name: 'Hexo', name: 'Hexo',
@@ -294,7 +295,6 @@ export const frameworks = [
}, },
}, },
dependency: 'hexo', dependency: 'hexo',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
}, },
{ {
@@ -332,7 +332,6 @@ export const frameworks = [
}, },
}, },
dependency: '@11ty/eleventy', dependency: '@11ty/eleventy',
getFsOutputDir: async () => '_site',
getOutputDirName: async () => '_site', getOutputDirName: async () => '_site',
cachePattern: '.cache/**', cachePattern: '.cache/**',
}, },
@@ -372,22 +371,6 @@ export const frameworks = [
}, },
}, },
dependency: '@docusaurus/core', dependency: '@docusaurus/core',
getFsOutputDir: async (dirPrefix: string) => {
const base = 'build';
try {
const location = join(dirPrefix, base);
const content = await readdir(location, { withFileTypes: true });
// If there is only one file in it that is a dir we'll use it as dist dir
if (content.length === 1 && content[0].isDirectory()) {
return join(base, content[0].name);
}
} catch (error) {
console.error(`Error detecting output directory: `, error);
}
return base;
},
getOutputDirName: async (dirPrefix: string) => { getOutputDirName: async (dirPrefix: string) => {
const base = 'build'; const base = 'build';
try { try {
@@ -527,21 +510,6 @@ export const frameworks = [
}, },
}, },
dependency: 'docusaurus', dependency: 'docusaurus',
getFsOutputDir: async (dirPrefix: string) => {
const base = 'build';
try {
const location = join(dirPrefix, base);
const content = await readdir(location, { withFileTypes: true });
// If there is only one file in it that is a dir we'll use it as dist dir
if (content.length === 1 && content[0].isDirectory()) {
return join(base, content[0].name);
}
} catch (error) {
console.error(`Error detecting output directory: `, error);
}
return base;
},
getOutputDirName: async (dirPrefix: string) => { getOutputDirName: async (dirPrefix: string) => {
const base = 'build'; const base = 'build';
try { try {
@@ -593,7 +561,6 @@ export const frameworks = [
}, },
}, },
dependency: 'preact-cli', dependency: 'preact-cli',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build', getOutputDirName: async () => 'build',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -650,7 +617,6 @@ export const frameworks = [
}, },
}, },
dependency: '@dojo/cli', dependency: '@dojo/cli',
getFsOutputDir: async () => 'output/dist',
getOutputDirName: async () => join('output', 'dist'), getOutputDirName: async () => join('output', 'dist'),
defaultRoutes: [ defaultRoutes: [
{ {
@@ -717,7 +683,6 @@ export const frameworks = [
}, },
}, },
dependency: 'ember-cli', dependency: 'ember-cli',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -772,7 +737,6 @@ export const frameworks = [
}, },
}, },
dependency: '@vue/cli-service', dependency: '@vue/cli-service',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -849,7 +813,6 @@ export const frameworks = [
}, },
}, },
dependency: '@scullyio/init', dependency: '@scullyio/init',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist/static', getOutputDirName: async () => 'dist/static',
}, },
{ {
@@ -886,7 +849,6 @@ export const frameworks = [
}, },
}, },
dependency: '@ionic/angular', dependency: '@ionic/angular',
getFsOutputDir: async () => 'www',
getOutputDirName: async () => 'www', getOutputDirName: async () => 'www',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -940,7 +902,6 @@ export const frameworks = [
}, },
}, },
dependency: '@angular/cli', dependency: '@angular/cli',
getFsOutputDir: async () => 'dist',
getOutputDirName: async (dirPrefix: string) => { getOutputDirName: async (dirPrefix: string) => {
const base = 'dist'; const base = 'dist';
try { try {
@@ -1008,7 +969,6 @@ export const frameworks = [
}, },
}, },
dependency: 'polymer-cli', dependency: 'polymer-cli',
getFsOutputDir: async () => 'build',
getOutputDirName: async (dirPrefix: string) => { getOutputDirName: async (dirPrefix: string) => {
const base = 'build'; const base = 'build';
try { try {
@@ -1078,7 +1038,6 @@ export const frameworks = [
}, },
}, },
dependency: 'sirv-cli', dependency: 'sirv-cli',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -1128,10 +1087,9 @@ export const frameworks = [
placeholder: 'svelte-kit dev', placeholder: 'svelte-kit dev',
}, },
outputDirectory: { outputDirectory: {
placeholder: 'public', value: 'public',
}, },
}, },
getFsOutputDir: async () => '.output',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
}, },
{ {
@@ -1168,7 +1126,6 @@ export const frameworks = [
}, },
}, },
dependency: '@ionic/react', dependency: '@ionic/react',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build', getOutputDirName: async () => 'build',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -1276,7 +1233,6 @@ export const frameworks = [
}, },
}, },
dependency: 'react-scripts', dependency: 'react-scripts',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build', getOutputDirName: async () => 'build',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -1378,7 +1334,6 @@ export const frameworks = [
}, },
}, },
dependency: 'gridsome', dependency: 'gridsome',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
}, },
{ {
@@ -1416,7 +1371,6 @@ export const frameworks = [
}, },
}, },
dependency: 'umi', dependency: 'umi',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -1470,7 +1424,6 @@ export const frameworks = [
}, },
}, },
dependency: 'sapper', dependency: 'sapper',
getFsOutputDir: async () => '__sapper__/export',
getOutputDirName: async () => '__sapper__/export', getOutputDirName: async () => '__sapper__/export',
}, },
{ {
@@ -1508,7 +1461,6 @@ export const frameworks = [
}, },
}, },
dependency: 'saber', dependency: 'saber',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -1577,7 +1529,6 @@ export const frameworks = [
}, },
}, },
dependency: '@stencil/core', dependency: '@stencil/core',
getFsOutputDir: async () => 'www',
getOutputDirName: async () => 'www', getOutputDirName: async () => 'www',
defaultRoutes: [ defaultRoutes: [
{ {
@@ -1666,7 +1617,6 @@ export const frameworks = [
}, },
}, },
dependency: 'nuxt', dependency: 'nuxt',
getFsOutputDir: async () => '.output',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
cachePattern: '.nuxt/**', cachePattern: '.nuxt/**',
defaultRoutes: [ defaultRoutes: [
@@ -1724,7 +1674,6 @@ export const frameworks = [
placeholder: 'RedwoodJS default', placeholder: 'RedwoodJS default',
}, },
}, },
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
}, },
{ {
@@ -1768,16 +1717,6 @@ export const frameworks = [
placeholder: '`public` or `publishDir` from the `config` file', placeholder: '`public` or `publishDir` from the `config` file',
}, },
}, },
getFsOutputDir: async (dirPrefix: string): Promise<string> => {
type HugoConfig = { publishDir?: string };
const config = await readConfigFile<HugoConfig>(
['config.json', 'config.yaml', 'config.toml'].map(fileName => {
return join(dirPrefix, fileName);
})
);
return (config && config.publishDir) || 'public';
},
getOutputDirName: async (dirPrefix: string): Promise<string> => { getOutputDirName: async (dirPrefix: string): Promise<string> => {
type HugoConfig = { publishDir?: string }; type HugoConfig = { publishDir?: string };
const config = await readConfigFile<HugoConfig>( const config = await readConfigFile<HugoConfig>(
@@ -1822,13 +1761,6 @@ export const frameworks = [
placeholder: '`_site` or `destination` from `_config.yml`', placeholder: '`_site` or `destination` from `_config.yml`',
}, },
}, },
getFsOutputDir: async (dirPrefix: string): Promise<string> => {
type JekyllConfig = { destination?: string };
const config = await readConfigFile<JekyllConfig>(
join(dirPrefix, '_config.yml')
);
return (config && config.destination) || '_site';
},
getOutputDirName: async (dirPrefix: string): Promise<string> => { getOutputDirName: async (dirPrefix: string): Promise<string> => {
type JekyllConfig = { destination?: string }; type JekyllConfig = { destination?: string };
const config = await readConfigFile<JekyllConfig>( const config = await readConfigFile<JekyllConfig>(
@@ -1870,7 +1802,6 @@ export const frameworks = [
value: 'public', value: 'public',
}, },
}, },
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
}, },
{ {
@@ -1905,7 +1836,6 @@ export const frameworks = [
value: 'build', value: 'build',
}, },
}, },
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build', getOutputDirName: async () => 'build',
cachePattern: '{vendor/bin,vendor/cache,vendor/bundle}/**', cachePattern: '{vendor/bin,vendor/cache,vendor/bundle}/**',
}, },
@@ -1940,7 +1870,6 @@ export const frameworks = [
value: 'public', value: 'public',
}, },
}, },
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public', getOutputDirName: async () => 'public',
defaultVersion: '0.13.0', defaultVersion: '0.13.0',
}, },
@@ -1980,7 +1909,6 @@ export const frameworks = [
}, },
}, },
dependency: 'vite', dependency: 'vite',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
}, },
{ {
@@ -2018,7 +1946,6 @@ export const frameworks = [
}, },
}, },
dependency: 'parcel', dependency: 'parcel',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist', getOutputDirName: async () => 'dist',
defaultRoutes: [ defaultRoutes: [
{ {

View File

@@ -162,9 +162,9 @@ export interface Framework {
dependency?: string; dependency?: string;
/** /**
* Function that returns the name of the directory that the framework outputs * Function that returns the name of the directory that the framework outputs
* its build results to. In some cases this is read from a configuration file. * its File System API build results to, usually called `.output`.
*/ */
getFsOutputDir: (dirPrefix: string) => Promise<string>; getFsOutputDir?: (dirPrefix: string) => Promise<string>;
/** /**
* Function that returns the name of the directory that the framework outputs * Function that returns the name of the directory that the framework outputs
* its STATIC build results to. In some cases this is read from a configuration file. * its STATIC build results to. In some cases this is read from a configuration file.

View File

@@ -1,6 +1,6 @@
{ {
"name": "vercel-plugin-middleware", "name": "vercel-plugin-middleware",
"version": "0.0.0-canary.7", "version": "0.0.0-canary.19",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "", "homepage": "",
@@ -30,6 +30,7 @@
"@types/node-fetch": "^2", "@types/node-fetch": "^2",
"@types/ua-parser-js": "0.7.36", "@types/ua-parser-js": "0.7.36",
"@types/uuid": "8.3.1", "@types/uuid": "8.3.1",
"@vercel/build-utils": "2.12.3-canary.42",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"cookie": "0.4.1", "cookie": "0.4.1",
"formdata-node": "4.3.1", "formdata-node": "4.3.1",

View File

@@ -1,4 +1,4 @@
import * as middleware from './_middleware'; import * as middleware from './_temp_middleware';
_ENTRIES = typeof _ENTRIES === 'undefined' ? {} : _ENTRIES; _ENTRIES = typeof _ENTRIES === 'undefined' ? {} : _ENTRIES;
_ENTRIES['middleware_pages/_middleware'] = { _ENTRIES['middleware_pages/_middleware'] = {
default: async function (ev) { default: async function (ev) {

View File

@@ -0,0 +1,52 @@
import path from 'path';
import * as esbuild from 'esbuild';
const processInjectFile = `
// envOverride is passed by esbuild plugin
const env = envOverride
function cwd() {
return '/'
}
function chdir(dir) {
throw new Error('process.chdir is not supported')
}
export const process = {
argv: [],
env,
chdir,
cwd,
};
`;
export function nodeProcessPolyfillPlugin({ env = {} } = {}): esbuild.Plugin {
return {
name: 'node-process-polyfill',
setup({ initialOptions, onResolve, onLoad }) {
onResolve({ filter: /_virtual-process-polyfill_\.js/ }, ({ path }) => {
return {
path,
sideEffects: false,
};
});
onLoad({ filter: /_virtual-process-polyfill_\.js/ }, () => {
const contents = `const envOverride = ${JSON.stringify(
env
)};\n${processInjectFile}`;
return {
loader: 'js',
contents,
};
});
const polyfills = [
path.resolve(__dirname, '_virtual-process-polyfill_.js'),
];
if (initialOptions.inject) {
initialOptions.inject.push(...polyfills);
} else {
initialOptions.inject = [...polyfills];
}
},
};
}

View File

@@ -5,6 +5,7 @@ import { promises as fsp } from 'fs';
import { IncomingMessage, ServerResponse } from 'http'; import { IncomingMessage, ServerResponse } from 'http';
import libGlob from 'glob'; import libGlob from 'glob';
import Proxy from 'http-proxy'; import Proxy from 'http-proxy';
import { updateFunctionsManifest } from '@vercel/build-utils';
import { run } from './websandbox'; import { run } from './websandbox';
import type { FetchEventResult } from './websandbox/types'; import type { FetchEventResult } from './websandbox/types';
@@ -16,13 +17,15 @@ import {
UrlWithParsedQuery, UrlWithParsedQuery,
} from 'url'; } from 'url';
import { toNodeHeaders } from './websandbox/utils'; import { toNodeHeaders } from './websandbox/utils';
import { nodeProcessPolyfillPlugin } from './esbuild-plugins';
const glob = util.promisify(libGlob); const glob = util.promisify(libGlob);
const SUPPORTED_EXTENSIONS = ['.js', '.ts']; const SUPPORTED_EXTENSIONS = ['.js', '.ts'];
// File name of the `entries.js` file that gets copied into the // File name of the `entries.js` file that gets copied into the
// project directory. Use a name that is unlikely to conflict. // project directory. Use a name that is unlikely to conflict.
const ENTRIES_NAME = '___vc_entries.js'; const TMP_ENTRIES_NAME = '.output/inputs/middleware/___vc_entries.js';
const TMP_MIDDLEWARE_BUNDLE = '.output/inputs/middleware/_temp_middleware.js';
async function getMiddlewareFile(workingDirectory: string) { async function getMiddlewareFile(workingDirectory: string) {
// Only the root-level `_middleware.*` files are considered. // Only the root-level `_middleware.*` files are considered.
@@ -52,17 +55,37 @@ async function getMiddlewareFile(workingDirectory: string) {
} }
export async function build({ workPath }: { workPath: string }) { export async function build({ workPath }: { workPath: string }) {
const entriesPath = join(workPath, ENTRIES_NAME); const entriesPath = join(workPath, TMP_ENTRIES_NAME);
const transientFilePath = join(workPath, TMP_MIDDLEWARE_BUNDLE);
const middlewareFile = await getMiddlewareFile(workPath); const middlewareFile = await getMiddlewareFile(workPath);
if (!middlewareFile) return; if (!middlewareFile) return;
console.log('Compiling middleware file: %j', middlewareFile); console.log('Compiling middleware file: %j', middlewareFile);
// Create `_ENTRIES` wrapper /**
await fsp.copyFile(join(__dirname, 'entries.js'), entriesPath); * Two builds happen here, because esbuild doesn't offer a way to add a banner
* to individual input files, and the entries wrapper relies on running in
// Build * non-strict mode to access the ENTRIES global.
*
* To work around this, we bundle the middleware directly and add
* 'use strict'; to make the entire bundle run in strict mode. We then bundle
* a second time, adding the global ENTRIES wrapper and preserving the
* 'use strict' for the entire scope of the original bundle.
*/
try { try {
await esbuild.build({
entryPoints: [middlewareFile],
bundle: true,
absWorkingDir: workPath,
outfile: transientFilePath,
banner: {
js: '"use strict";',
},
plugins: [nodeProcessPolyfillPlugin({ env: process.env })],
format: 'cjs',
});
// Create `_ENTRIES` wrapper
await fsp.copyFile(join(__dirname, 'entries.js'), entriesPath);
await esbuild.build({ await esbuild.build({
entryPoints: [entriesPath], entryPoints: [entriesPath],
bundle: true, bundle: true,
@@ -70,29 +93,24 @@ export async function build({ workPath }: { workPath: string }) {
outfile: join(workPath, '.output/server/pages/_middleware.js'), outfile: join(workPath, '.output/server/pages/_middleware.js'),
}); });
} finally { } finally {
await fsp.unlink(transientFilePath);
await fsp.unlink(entriesPath); await fsp.unlink(entriesPath);
} }
// Write middleware manifest const fileName = basename(middlewareFile);
const middlewareManifest = { const pages: { [key: string]: any } = {};
version: 1,
sortedMiddleware: ['/'], pages[fileName] = {
middleware: { runtime: 'web',
'/': { env: [],
env: [], files: ['server/pages/_middleware.js'],
files: ['server/pages/_middleware.js'], name: 'pages/_middleware',
name: 'pages/_middleware', page: '/',
page: '/', regexp: '^/.*$',
regexp: '^/.*$', sortingIndex: 1,
},
},
}; };
const middlewareManifestData = JSON.stringify(middlewareManifest, null, 2);
const middlewareManifestPath = join( await updateFunctionsManifest({ workPath, pages });
workPath,
'.output/server/middleware-manifest.json'
);
await fsp.writeFile(middlewareManifestPath, middlewareManifestData);
} }
const stringifyQuery = (req: IncomingMessage, query: ParsedUrlQuery) => { const stringifyQuery = (req: IncomingMessage, query: ParsedUrlQuery) => {

View File

@@ -114,6 +114,7 @@ export async function run(params: {
const content = readFileSync(params.path, 'utf-8'); const content = readFileSync(params.path, 'utf-8');
const esBuildResult = esbuild.transformSync(content, { const esBuildResult = esbuild.transformSync(content, {
format: 'cjs', format: 'cjs',
banner: '"use strict";',
}); });
const x = vm.runInNewContext(m.wrap(esBuildResult.code), cache.sandbox, { const x = vm.runInNewContext(m.wrap(esBuildResult.code), cache.sandbox, {
filename: params.path, filename: params.path,
@@ -163,6 +164,7 @@ function sandboxRequire(referrer: string, specifier: string) {
const transformOptions: esbuild.TransformOptions = { const transformOptions: esbuild.TransformOptions = {
format: 'cjs', format: 'cjs',
banner: '"use strict";',
}; };
if (extname(resolved) === '.json') { if (extname(resolved) === '.json') {
transformOptions.loader = 'json'; transformOptions.loader = 'json';

View File

@@ -2,8 +2,8 @@
exports[`build() should build simple middleware 1`] = ` exports[`build() should build simple middleware 1`] = `
Object { Object {
"middleware": Object { "pages": Object {
"/": Object { "_middleware.js": Object {
"env": Array [], "env": Array [],
"files": Array [ "files": Array [
"server/pages/_middleware.js", "server/pages/_middleware.js",
@@ -11,11 +11,10 @@ Object {
"name": "pages/_middleware", "name": "pages/_middleware",
"page": "/", "page": "/",
"regexp": "^/.*$", "regexp": "^/.*$",
"runtime": "web",
"sortingIndex": 1,
}, },
}, },
"sortedMiddleware": Array [ "version": 2,
"/",
],
"version": 1,
} }
`; `;

View File

@@ -3,6 +3,30 @@ import { promises as fsp } from 'fs';
import { build } from '../src'; import { build } from '../src';
import { Response } from 'node-fetch'; import { Response } from 'node-fetch';
const setupFixture = async (fixture: string) => {
const fixturePath = join(__dirname, `fixtures/${fixture}`);
await build({
workPath: fixturePath,
});
const functionsManifest = JSON.parse(
await fsp.readFile(
join(fixturePath, '.output/functions-manifest.json'),
'utf8'
)
);
const outputFile = join(fixturePath, '.output/server/pages/_middleware.js');
expect(await fsp.stat(outputFile)).toBeTruthy();
require(outputFile);
//@ts-ignore
const middleware = global._ENTRIES['middleware_pages/_middleware'].default;
return {
middleware,
functionsManifest,
};
};
describe('build()', () => { describe('build()', () => {
beforeEach(() => { beforeEach(() => {
//@ts-ignore //@ts-ignore
@@ -15,25 +39,9 @@ describe('build()', () => {
delete global._ENTRIES; delete global._ENTRIES;
}); });
it('should build simple middleware', async () => { it('should build simple middleware', async () => {
const fixture = join(__dirname, 'fixtures/simple'); const { functionsManifest, middleware } = await setupFixture('simple');
await build({
workPath: fixture,
});
const middlewareManifest = JSON.parse( expect(functionsManifest).toMatchSnapshot();
await fsp.readFile(
join(fixture, '.output/server/middleware-manifest.json'),
'utf8'
)
);
expect(middlewareManifest).toMatchSnapshot();
const outputFile = join(fixture, '.output/server/pages/_middleware.js');
expect(await fsp.stat(outputFile)).toBeTruthy();
require(outputFile);
//@ts-ignore
const middleware = global._ENTRIES['middleware_pages/_middleware'].default;
expect(typeof middleware).toStrictEqual('function'); expect(typeof middleware).toStrictEqual('function');
const handledResponse = await middleware({ const handledResponse = await middleware({
request: { request: {
@@ -54,4 +62,38 @@ describe('build()', () => {
(unhandledResponse.response as Response).headers.get('x-middleware-next') (unhandledResponse.response as Response).headers.get('x-middleware-next')
).toEqual('1'); ).toEqual('1');
}); });
it('should build simple middleware with env vars', async () => {
const expectedEnvVar = 'expected-env-var';
const fixture = join(__dirname, 'fixtures/env');
process.env.ENV_VAR_SHOULD_BE_DEFINED = expectedEnvVar;
await build({
workPath: fixture,
});
// env var should be inlined in the output
delete process.env.ENV_VAR_SHOULD_BE_DEFINED;
const outputFile = join(fixture, '.output/server/pages/_middleware.js');
expect(await fsp.stat(outputFile)).toBeTruthy();
require(outputFile);
//@ts-ignore
const middleware = global._ENTRIES['middleware_pages/_middleware'].default;
expect(typeof middleware).toStrictEqual('function');
const handledResponse = await middleware({
request: {},
});
expect(String(handledResponse.response.body)).toEqual(expectedEnvVar);
expect(
(handledResponse.response as Response).headers.get('x-middleware-next')
).toEqual(null);
});
it('should create a middleware that runs in strict mode', async () => {
const { middleware } = await setupFixture('use-strict');
const response = await middleware({
request: {},
});
expect(String(response.response.body)).toEqual('is strict mode? yes');
});
}); });

View File

@@ -0,0 +1,3 @@
export default req => {
return new Response(process.env.ENV_VAR_SHOULD_BE_DEFINED);
};

View File

@@ -0,0 +1,6 @@
export default function (req) {
const isStrict = (function () {
return !this;
})();
return new Response('is strict mode? ' + (isStrict ? 'yes' : 'no'));
}

View File

@@ -1,7 +1,7 @@
{ {
"private": false, "private": false,
"name": "vercel-plugin-go", "name": "vercel-plugin-go",
"version": "1.0.0-canary.6", "version": "1.0.0-canary.30",
"main": "dist/index.js", "main": "dist/index.js",
"license": "MIT", "license": "MIT",
"files": [ "files": [
@@ -17,7 +17,7 @@
"prepublishOnly": "tsc" "prepublishOnly": "tsc"
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "2.12.3-canary.21", "@vercel/build-utils": "2.12.3-canary.42",
"@vercel/go": "1.2.4-canary.4" "@vercel/go": "1.2.4-canary.4"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,6 +1,6 @@
import { convertRuntimeToPlugin } from '@vercel/build-utils'; import { convertRuntimeToPlugin } from '@vercel/build-utils';
import * as go from '@vercel/go'; import * as go from '@vercel/go';
export const build = convertRuntimeToPlugin(go.build, '.go'); export const build = convertRuntimeToPlugin(go.build, 'vercel-plugin-go', '.go');
export const startDevServer = go.startDevServer; export const startDevServer = go.startDevServer;

View File

@@ -1,6 +1,6 @@
{ {
"name": "vercel-plugin-node", "name": "vercel-plugin-node",
"version": "1.12.2-canary.12", "version": "1.12.2-canary.34",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js", "homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -34,12 +34,12 @@
"@types/node-fetch": "2", "@types/node-fetch": "2",
"@types/test-listen": "1.1.0", "@types/test-listen": "1.1.0",
"@types/yazl": "2.4.2", "@types/yazl": "2.4.2",
"@vercel/build-utils": "2.12.3-canary.21", "@vercel/build-utils": "2.12.3-canary.42",
"@vercel/fun": "1.0.3", "@vercel/fun": "1.0.3",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"@vercel/nft": "0.14.0", "@vercel/nft": "0.14.0",
"@vercel/node-bridge": "2.1.1-canary.2", "@vercel/node-bridge": "2.1.1-canary.2",
"@vercel/static-config": "0.0.1-canary.0", "@vercel/static-config": "0.0.1-canary.1",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"content-type": "1.0.4", "content-type": "1.0.4",
"cookie": "0.4.0", "cookie": "0.4.0",

View File

@@ -40,6 +40,7 @@ import {
walkParentDirs, walkParentDirs,
normalizePath, normalizePath,
runPackageJsonScript, runPackageJsonScript,
getInputHash,
} from '@vercel/build-utils'; } from '@vercel/build-utils';
import { FromSchema } from 'json-schema-to-ts'; import { FromSchema } from 'json-schema-to-ts';
import { getConfig, BaseFunctionConfigSchema } from '@vercel/static-config'; import { getConfig, BaseFunctionConfigSchema } from '@vercel/static-config';
@@ -47,8 +48,6 @@ import { AbortController } from 'abort-controller';
import { Register, register } from './typescript'; import { Register, register } from './typescript';
import { pageToRoute } from './router/page-to-route'; import { pageToRoute } from './router/page-to-route';
import { isDynamicRoute } from './router/is-dynamic'; import { isDynamicRoute } from './router/is-dynamic';
import crypto from 'crypto';
import type { VercelConfig } from '@vercel/client';
export { shouldServe }; export { shouldServe };
export { export {
@@ -380,13 +379,7 @@ function getAWSLambdaHandler(entrypoint: string, config: FunctionConfig) {
} }
// TODO NATE: turn this into a `@vercel/plugin-utils` helper function? // TODO NATE: turn this into a `@vercel/plugin-utils` helper function?
export async function build({ export async function build({ workPath }: { workPath: string }) {
vercelConfig,
workPath,
}: {
vercelConfig: VercelConfig;
workPath: string;
}) {
const project = new Project(); const project = new Project();
const entrypoints = await glob('api/**/*.[jt]s', workPath); const entrypoints = await glob('api/**/*.[jt]s', workPath);
const installedPaths = new Set<string>(); const installedPaths = new Set<string>();
@@ -408,14 +401,13 @@ export async function build({
getConfig(project, absEntrypoint, FunctionConfigSchema) || {}; getConfig(project, absEntrypoint, FunctionConfigSchema) || {};
// No config exported means "node", but if there is a config // No config exported means "node", but if there is a config
// and "runtime" is defined, but it is not "node" then don't // and "use" is defined, but it is not "node" then don't
// compile this file. // compile this file.
if (config.runtime && config.runtime !== 'node') { if (config.use && config.use !== 'node') {
continue; continue;
} }
await buildEntrypoint({ await buildEntrypoint({
vercelConfig,
workPath, workPath,
entrypoint, entrypoint,
config, config,
@@ -425,23 +417,18 @@ export async function build({
} }
export async function buildEntrypoint({ export async function buildEntrypoint({
vercelConfig,
workPath, workPath,
entrypoint, entrypoint,
config, config,
installedPaths, installedPaths,
}: { }: {
vercelConfig: VercelConfig;
workPath: string; workPath: string;
entrypoint: string; entrypoint: string;
config: FunctionConfig; config: FunctionConfig;
installedPaths?: Set<string>; installedPaths?: Set<string>;
}) { }) {
// Unique hash that will be used as directory name for `.output`. // Unique hash that will be used as directory name for `.output`.
const entrypointHash = crypto const entrypointHash = 'api-routes-node-' + getInputHash(entrypoint);
.createHash('sha256')
.update(entrypoint)
.digest('hex');
const outputDirPath = join(workPath, '.output'); const outputDirPath = join(workPath, '.output');
const { dir, name } = parsePath(entrypoint); const { dir, name } = parsePath(entrypoint);
@@ -561,7 +548,7 @@ export async function buildEntrypoint({
runtime: nodeVersion.runtime, runtime: nodeVersion.runtime,
}, },
}; };
await updateFunctionsManifest({ vercelConfig, workPath, pages }); await updateFunctionsManifest({ workPath, pages });
// Update the `routes-mainifest.json` file with the wildcard route // Update the `routes-mainifest.json` file with the wildcard route
// when the entrypoint is dynamic (i.e. `/api/[id].ts`). // when the entrypoint is dynamic (i.e. `/api/[id].ts`).

View File

@@ -143,16 +143,7 @@ function withFixture<T>(
await runNpmInstall(fixture); await runNpmInstall(fixture);
} }
let vercelConfig = {}; await build({ workPath: fixture });
try {
vercelConfig = JSON.parse(
await fsp.readFile(path.join(fixture, 'vercel.json'), 'utf8')
);
} catch (e) {
// Consume error
}
await build({ vercelConfig, workPath: fixture });
try { try {
return await t({ fixture, fetch }); return await t({ fixture, fetch });

View File

@@ -1,7 +1,7 @@
{ {
"private": false, "private": false,
"name": "vercel-plugin-python", "name": "vercel-plugin-python",
"version": "1.0.0-canary.7", "version": "1.0.0-canary.31",
"main": "dist/index.js", "main": "dist/index.js",
"license": "MIT", "license": "MIT",
"files": [ "files": [
@@ -17,8 +17,8 @@
"prepublishOnly": "tsc" "prepublishOnly": "tsc"
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "2.12.3-canary.21", "@vercel/build-utils": "2.12.3-canary.42",
"@vercel/python": "2.1.2-canary.0" "@vercel/python": "2.1.2-canary.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "*", "@types/node": "*",

View File

@@ -1,6 +1,6 @@
import { convertRuntimeToPlugin } from '@vercel/build-utils'; import { convertRuntimeToPlugin } from '@vercel/build-utils';
import * as python from '@vercel/python'; import * as python from '@vercel/python';
export const build = convertRuntimeToPlugin(python.build, '.py'); export const build = convertRuntimeToPlugin(python.build, 'vercel-plugin-python', '.py');
//export const startDevServer = python.startDevServer; //export const startDevServer = python.startDevServer;

View File

@@ -1,7 +1,7 @@
{ {
"private": false, "private": false,
"name": "vercel-plugin-ruby", "name": "vercel-plugin-ruby",
"version": "1.0.0-canary.5", "version": "1.0.0-canary.30",
"main": "dist/index.js", "main": "dist/index.js",
"license": "MIT", "license": "MIT",
"files": [ "files": [
@@ -17,8 +17,8 @@
"prepublishOnly": "tsc" "prepublishOnly": "tsc"
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "2.12.3-canary.21", "@vercel/build-utils": "2.12.3-canary.42",
"@vercel/ruby": "1.2.8-canary.4" "@vercel/ruby": "1.2.10-canary.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "*", "@types/node": "*",

View File

@@ -1,6 +1,6 @@
import { convertRuntimeToPlugin } from '@vercel/build-utils'; import { convertRuntimeToPlugin } from '@vercel/build-utils';
import * as ruby from '@vercel/ruby'; import * as ruby from '@vercel/ruby';
export const build = convertRuntimeToPlugin(ruby.build, '.rb'); export const build = convertRuntimeToPlugin(ruby.build, 'vercel-plugin-ruby', '.rb');
//export const startDevServer = ruby.startDevServer; //export const startDevServer = ruby.startDevServer;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/python", "name": "@vercel/python",
"version": "2.1.2-canary.0", "version": "2.1.2-canary.2",
"main": "./dist/index.js", "main": "./dist/index.js",
"license": "MIT", "license": "MIT",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python", "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",

View File

@@ -135,6 +135,11 @@ export async function installRequirementsFile({
meta, meta,
args = [], args = [],
}: InstallRequirementsFileArg) { }: InstallRequirementsFileArg) {
// The Vercel platform already handles `requirements.txt` for frontend projects,
// but the installation logic there is different, because it seems to install all
// of the dependencies globally, whereas, for this Runtime, we want it to happen only
// locally, so we'll run a separate installation.
if ( if (
meta.isDev && meta.isDev &&
(await areRequirementsInstalled(pythonPath, filePath, workPath)) (await areRequirementsInstalled(pythonPath, filePath, workPath))

View File

@@ -1,4 +1,4 @@
import { join, dirname } from 'path'; import { join, dirname, relative } from 'path';
import execa from 'execa'; import execa from 'execa';
import { import {
ensureDir, ensureDir,
@@ -85,10 +85,12 @@ export async function build({
}: BuildOptions) { }: BuildOptions) {
await download(files, workPath, meta); await download(files, workPath, meta);
const entrypointFsDirname = join(workPath, dirname(entrypoint)); const entrypointFsDirname = join(workPath, dirname(entrypoint));
const gemfileName = 'Gemfile';
const gemfilePath = await walkParentDirs({ const gemfilePath = await walkParentDirs({
base: workPath, base: workPath,
start: entrypointFsDirname, start: entrypointFsDirname,
filename: 'Gemfile', filename: gemfileName,
}); });
const gemfileContents = gemfilePath const gemfileContents = gemfilePath
? await readFile(gemfilePath, 'utf8') ? await readFile(gemfilePath, 'utf8')
@@ -130,15 +132,24 @@ export async function build({
'did not find a vendor directory but found a Gemfile, bundling gems...' 'did not find a vendor directory but found a Gemfile, bundling gems...'
); );
// try installing. this won't work if native extesions are required. const fileAtRoot = relative(workPath, gemfilePath) === gemfileName;
// if that's the case, gems should be vendored locally before deploying.
try { // If the `Gemfile` is located in the Root Directory of the project and
await bundleInstall(bundlerPath, bundleDir, gemfilePath); // the new File System API is used (`avoidTopLevelInstall`), the Install Command
} catch (err) { // will have already installed its dependencies, so we don't need to do it again.
debug( if (meta.avoidTopLevelInstall && fileAtRoot) {
'unable to build gems from Gemfile. vendor the gems locally with "bundle install --deployment" and retry.' debug('Skipping `bundle install` — already handled by Install Command');
); } else {
throw err; // try installing. this won't work if native extesions are required.
// if that's the case, gems should be vendored locally before deploying.
try {
await bundleInstall(bundlerPath, bundleDir, gemfilePath);
} catch (err) {
debug(
'unable to build gems from Gemfile. vendor the gems locally with "bundle install --deployment" and retry.'
);
throw err;
}
} }
} }
} else { } else {

View File

@@ -9,13 +9,24 @@ interface RubyVersion extends NodeVersion {
const allOptions: RubyVersion[] = [ const allOptions: RubyVersion[] = [
{ major: 2, minor: 7, range: '2.7.x', runtime: 'ruby2.7' }, { major: 2, minor: 7, range: '2.7.x', runtime: 'ruby2.7' },
{ major: 2, minor: 5, range: '2.5.x', runtime: 'ruby2.5' }, {
major: 2,
minor: 5,
range: '2.5.x',
runtime: 'ruby2.5',
discontinueDate: new Date('2021-11-30'),
},
]; ];
function getLatestRubyVersion(): RubyVersion { function getLatestRubyVersion(): RubyVersion {
return allOptions[0]; return allOptions[0];
} }
function isDiscontinued({ discontinueDate }: RubyVersion): boolean {
const today = Date.now();
return discontinueDate !== undefined && discontinueDate.getTime() <= today;
}
function getRubyPath(meta: Meta, gemfileContents: string) { function getRubyPath(meta: Meta, gemfileContents: string) {
let selection = getLatestRubyVersion(); let selection = getLatestRubyVersion();
if (meta.isDev) { if (meta.isDev) {
@@ -37,8 +48,20 @@ function getRubyPath(meta: Meta, gemfileContents: string) {
if (!found) { if (!found) {
throw new NowBuildError({ throw new NowBuildError({
code: 'RUBY_INVALID_VERSION', code: 'RUBY_INVALID_VERSION',
message: 'Found `Gemfile` with invalid Ruby version: `' + line + '`.', message: `Found \`Gemfile\` with invalid Ruby version: \`${line}.\``,
link: 'https://vercel.com/docs/runtimes#official-runtimes/ruby/ruby-version', link: 'http://vercel.link/ruby-version',
});
}
if (isDiscontinued(selection)) {
const latest = getLatestRubyVersion();
const intro = `Found \`Gemfile\` with discontinued Ruby version: \`${line}.\``;
const hint = `Please set \`ruby "~> ${latest.range}"\` in your \`Gemfile\` to use Ruby ${latest.range}.`;
const upstream =
'This change is the result of a decision made by an upstream infrastructure provider (AWS).';
throw new NowBuildError({
code: 'RUBY_DISCONTINUED_VERSION',
link: 'http://vercel.link/ruby-version',
message: `${intro} ${hint} ${upstream}`,
}); });
} }
} }
@@ -66,6 +89,23 @@ export async function installBundler(meta: Meta, gemfileContents: string) {
gemfileContents gemfileContents
); );
// If the new File System API is used (`avoidTopLevelInstall`), the Install Command
// will have already installed the dependencies, so we don't need to do it again.
if (meta.avoidTopLevelInstall) {
debug(
`Skipping bundler installation, already installed by Install Command`
);
return {
gemHome,
rubyPath,
gemPath,
vendorPath,
runtime,
bundlerPath: join(gemHome, 'bin', 'bundler'),
};
}
debug('installing bundler...'); debug('installing bundler...');
await execa(gemPath, ['install', 'bundler', '--no-document'], { await execa(gemPath, ['install', 'bundler', '--no-document'], {
stdio: 'pipe', stdio: 'pipe',

View File

@@ -1,7 +1,7 @@
{ {
"name": "@vercel/ruby", "name": "@vercel/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>", "author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "1.2.8-canary.4", "version": "1.2.10-canary.0",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby", "homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",

View File

@@ -0,0 +1,2 @@
---
BUNDLE_PATH: "vendor/bundle"

View File

@@ -2,6 +2,6 @@
source "https://rubygems.org" source "https://rubygems.org"
ruby "~> 2.5.0" ruby "~> 2.7.0"
gem "cowsay", "~> 0.3.0" gem "cowsay", "~> 0.3.0"

View File

@@ -0,0 +1,16 @@
GEM
remote: https://rubygems.org/
specs:
cowsay (0.3.0)
PLATFORMS
x86_64-darwin-21
DEPENDENCIES
cowsay (~> 0.3.0)
RUBY VERSION
ruby 2.7.5p203
BUNDLED WITH
2.2.22

View File

@@ -1,6 +1,5 @@
{ {
"version": 2, "version": 2,
"builds": [{ "src": "index.rb", "use": "@vercel/ruby" }], "builds": [{ "src": "index.rb", "use": "@vercel/ruby" }],
"build": { "env": { "RUBY_VERSION": "2.5.x" } },
"probes": [{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" }] "probes": [{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" }]
} }

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