Compare commits

...

44 Commits

Author SHA1 Message Date
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
Andy Bitz
cc7b2691c1 Publish Canary
- @vercel/build-utils@2.12.3-canary.21
 - vercel@23.1.3-canary.39
 - @vercel/client@10.2.3-canary.22
 - @vercel/frameworks@0.5.1-canary.13
 - vercel-plugin-go@1.0.0-canary.6
 - vercel-plugin-node@1.12.2-canary.12
 - vercel-plugin-python@1.0.0-canary.7
 - vercel-plugin-ruby@1.0.0-canary.5
 - @vercel/python@2.1.2-canary.0
2021-11-24 17:41:53 +01:00
Leo Lamprecht
3a75c37adc Added Remix to the list of frameworks (#7061)
* Added Remix to the list of frameworks

* Replaced Development Command

* Polished config

* Added headers

* Added example

* Fixed logo

* Trigger a Deployment

* Added vercel.json

* Port is automatically parsed through remix.config.js

* Fixed sorting

* Moved it down a bit

* Trigger a Deployment

* Removed useless property

* Reverted readme

* Fully reverted readme
2021-11-24 17:40:55 +01:00
Steven
acd0e84e73 [cli] Pass vercel.json to plugins during vercel build (#7059)
* [cli] Pass vercel.json to plugins during `vercel build`

* Add missing `vercelConfig` param

* Add type

* Fix test

* Update test
2021-11-23 19:12:57 -05:00
Jared Palmer
49087d5aac Change nextjs detection (#7020)
Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
2021-11-23 19:06:36 +01:00
Steven
7d7f3df980 Publish Stable
- @vercel/python@2.1.1
2021-11-23 09:04:08 -05:00
Steven
5cf0c316e9 Publish Stable
- @vercel/python@2.1.0
2021-11-23 08:57:58 -05:00
Steven
f4501433c8 Publish Canary
- vercel@23.1.3-canary.38
 - vercel-plugin-python@1.0.0-canary.6
 - @vercel/python@2.0.6-canary.7
2021-11-22 18:43:37 -05:00
Steven
19831593ce [python] Add warning for deprecated version (#7053)
- Print warning when python 3.6 is selected
- Print warning when invalid python version is selected
2021-11-22 23:36:30 +00:00
Steven
5d85bb1426 [tests] Bump cancel action to 0.9.1 (#7055) 2021-11-22 17:24:35 -05:00
162 changed files with 26143 additions and 3338 deletions

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: styfle/cancel-workflow-action@0.4.1
- uses: styfle/cancel-workflow-action@0.9.1
with:
workflow_id: 849295, 849296, 849297, 849298
access_token: ${{ github.token }}

View File

@@ -332,7 +332,7 @@ This is an abstract enumeration type that is implemented by one of the following
- `nodejs10.x`
- `go1.x`
- `java11`
- `python3.8`
- `python3.9`
- `python3.6`
- `dotnetcore2.1`
- `ruby2.5`
@@ -398,12 +398,12 @@ This utility allows you to _scan_ the filesystem and return a [`Files`](#files)
The following trivial example downloads everything to the filesystem, only to return it back (therefore just re-creating the passed-in [`Files`](#files)):
```js
const { glob, download } = require('@vercel/build-utils')
const { glob, download } = require('@vercel/build-utils');
exports.build = ({ files, workPath }) => {
await download(files, workPath)
return glob('**', workPath)
}
await download(files, workPath);
return glob('**', workPath);
};
```
### `getWritableDirectory()`

View File

@@ -5,7 +5,7 @@
"description": "API for the vercel/vercel repo",
"main": "index.js",
"scripts": {
"vercel-build": "yarn --cwd .. && node ../utils/run.js build all"
"vercel-build": "node ../utils/run.js build all"
},
"dependencies": {
"@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

8
examples/remix/.gitignore vendored Normal file
View File

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

34
examples/remix/README.md Normal file
View File

@@ -0,0 +1,34 @@
# Welcome to Remix!
- [Remix Docs](https://remix.run/docs)
## Deployment
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
npm i -g vercel
vercel
```
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).
## Development
To run your Remix app locally, make sure your project's local dependencies are installed:
```sh
npm install
```
Afterwards, start the Remix development server like so:
```sh
npm run dev
```
Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go!
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.

View File

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

View File

@@ -0,0 +1,4 @@
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";
hydrate(<RemixBrowser />, document);

View File

@@ -0,0 +1,21 @@
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders
});
}

178
examples/remix/app/root.tsx Normal file
View File

@@ -0,0 +1,178 @@
import {
Link,
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useCatch
} from "remix";
import type { LinksFunction } from "remix";
import globalStylesUrl from "~/styles/global.css";
import darkStylesUrl from "~/styles/dark.css";
// https://remix.run/api/app#links
export let links: LinksFunction = () => {
return [
{ rel: "stylesheet", href: globalStylesUrl },
{
rel: "stylesheet",
href: darkStylesUrl,
media: "(prefers-color-scheme: dark)"
}
];
};
// https://remix.run/api/conventions#default-export
// https://remix.run/api/conventions#route-filenames
export default function App() {
return (
<Document>
<Layout>
<Outlet />
</Layout>
</Document>
);
}
// https://remix.run/docs/en/v1/api/conventions#errorboundary
export function ErrorBoundary({ error }: { error: Error }) {
console.error(error);
return (
<Document title="Error!">
<Layout>
<div>
<h1>There was an error</h1>
<p>{error.message}</p>
<hr />
<p>
Hey, developer, you should replace this with what you want your
users to see.
</p>
</div>
</Layout>
</Document>
);
}
// https://remix.run/docs/en/v1/api/conventions#catchboundary
export function CatchBoundary() {
let caught = useCatch();
let message;
switch (caught.status) {
case 401:
message = (
<p>
Oops! Looks like you tried to visit a page that you do not have access
to.
</p>
);
break;
case 404:
message = (
<p>Oops! Looks like you tried to visit a page that does not exist.</p>
);
break;
default:
throw new Error(caught.data || caught.statusText);
}
return (
<Document title={`${caught.status} ${caught.statusText}`}>
<Layout>
<h1>
{caught.status}: {caught.statusText}
</h1>
{message}
</Layout>
</Document>
);
}
function Document({
children,
title
}: {
children: React.ReactNode;
title?: string;
}) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{title ? <title>{title}</title> : null}
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === "development" && <LiveReload />}
</body>
</html>
);
}
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 (
<svg
viewBox="0 0 659 165"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
aria-labelledby="remix-run-logo-title"
role="img"
width="106"
height="30"
fill="currentColor"
>
<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="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" />
<path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" />
<path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" />
<path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" />
</svg>
);
}

View File

@@ -0,0 +1,44 @@
import { Outlet } from "remix";
import type { MetaFunction, LinksFunction } from "remix";
import stylesUrl from "~/styles/demos/about.css";
export let meta: MetaFunction = () => {
return {
title: "About Remix"
};
};
export let links: LinksFunction = () => {
return [{ rel: "stylesheet", href: stylesUrl }];
};
export default function Index() {
return (
<div className="about">
<div className="about__intro">
<h2>About Us</h2>
<p>
Ok, so this page isn't really <em>about us</em>, but we did want to
show you a few more things Remix can do.
</p>
<p>
Did you notice that things look a little different on this page? The
CSS that we import in the route file and include in its{" "}
<code>links</code> export is only included on this route and its
children.
</p>
<p>
Wait a sec...<em>its children</em>? To understand what we mean by
this,{" "}
<a href="https://remix.run/tutorial/4-nested-routes-params">
read all about nested routes in the docs
</a>
.
</p>
<hr />
<Outlet />
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { Link } from "remix";
export default function AboutIndex() {
return (
<div>
<p>
You are looking at the index route for the <code>/about</code> URL
segment, but there are nested routes as well!
</p>
<p>
<strong>
<Link to="whoa">Check out one of them here.</Link>
</strong>
</p>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { Link } from "remix";
export default function AboutIndex() {
return (
<div>
<p>
Whoa, this is a nested route! We render the <code>/about</code> layout
route component, and its <code>Outlet</code> renders our route
component. 🤯
</p>
<p>
<strong>
<Link to="..">
Go back to the <code>/about</code> index.
</Link>
</strong>
</p>
</div>
);
}

View File

@@ -0,0 +1,101 @@
import { useEffect, useRef } from "react";
import type { ActionFunction } from "remix";
import { Form, json, useActionData, redirect } from "remix";
export function meta() {
return { title: "Actions Demo" };
}
// When your form sends a POST, the action is called on the server.
// - https://remix.run/api/conventions#action
// - https://remix.run/guides/data-updates
export let action: ActionFunction = async ({ request }) => {
let formData = await request.formData();
let answer = formData.get("answer");
// Typical action workflows start with validating the form data that just came
// over the network. Clientside validation is fine, but you definitely need it
// server side. If there's a problem, return the the data and the component
// can render it.
if (typeof answer !== "string") {
return json("Come on, at least try!", { status: 400 });
}
if (answer !== "egg") {
return json(`Sorry, ${answer} is not right.`, { status: 400 });
}
// Finally, if the data is valid, you'll typically write to a database or send or
// email or log the user in, etc. It's recommended to redirect after a
// successful action, even if it's to the same place so that non-JavaScript workflows
// from the browser doesn't repost the data if the user clicks back.
return redirect("/demos/correct");
};
export default function ActionsDemo() {
// https://remix.run/api/remix#useactiondata
let actionMessage = useActionData<string>();
let answerRef = useRef<HTMLInputElement>(null);
// This form works without JavaScript, but when we have JavaScript we can make
// the experience better by selecting the input on wrong answers! Go ahead, disable
// JavaScript in your browser and see what happens.
useEffect(() => {
if (actionMessage && answerRef.current) {
answerRef.current.select();
}
}, [actionMessage]);
return (
<div className="remix__page">
<main>
<h2>Actions!</h2>
<p>
This form submission will send a post request that we handle in our
`action` export. Any route can export an action to handle data
mutations.
</p>
<Form method="post" className="remix__form">
<h3>Post an Action</h3>
<p>
<i>What is more useful when it is broken?</i>
</p>
<label>
<div>Answer:</div>
<input ref={answerRef} name="answer" type="text" />
</label>
<div>
<button>Answer!</button>
</div>
{actionMessage ? (
<p>
<b>{actionMessage}</b>
</p>
) : null}
</Form>
</main>
<aside>
<h3>Additional Resources</h3>
<ul>
<li>
Guide:{" "}
<a href="https://remix.run/guides/data-writes">Data Writes</a>
</li>
<li>
API:{" "}
<a href="https://remix.run/api/conventions#action">
Route Action Export
</a>
</li>
<li>
API:{" "}
<a href="https://remix.run/api/remix#useactiondata">
<code>useActionData</code>
</a>
</li>
</ul>
</aside>
</div>
);
}

View File

@@ -0,0 +1,3 @@
export default function NiceWork() {
return <h1>You got it right!</h1>;
}

View File

@@ -0,0 +1,43 @@
import { useCatch, Link, json, useLoaderData, Outlet } from "remix";
export function meta() {
return { title: "Boundaries Demo" };
}
export default function Boundaries() {
return (
<div className="remix__page">
<main>
<Outlet />
</main>
<aside>
<h2>Click these Links</h2>
<ul>
<li>
<Link to=".">Start over</Link>
</li>
<li>
<Link to="one">
Param: <i>one</i>
</Link>
</li>
<li>
<Link to="two">
Param: <i>two</i>
</Link>
</li>
<li>
<Link to="this-record-does-not-exist">This will be a 404</Link>
</li>
<li>
<Link to="shh-its-a-secret">And this will be 401 Unauthorized</Link>
</li>
<li>
<Link to="kaboom">This one will throw an error</Link>
</li>
</ul>
</aside>
</div>
);
}

View File

@@ -0,0 +1,110 @@
import { useCatch, Link, json, useLoaderData } from "remix";
import type { LoaderFunction, MetaFunction } from "remix";
// The `$` in route filenames becomes a pattern that's parsed from the URL and
// passed to your loaders so you can look up data.
// - https://remix.run/api/conventions#loader-params
export let loader: LoaderFunction = async ({ params }) => {
// pretend like we're using params.id to look something up in the db
if (params.id === "this-record-does-not-exist") {
// If the record doesn't exist we can't render the route normally, so
// instead we throw a 404 reponse to stop running code here and show the
// user the catch boundary.
throw new Response("Not Found", { status: 404 });
}
// now pretend like the record exists but the user just isn't authorized to
// see it.
if (params.id === "shh-its-a-secret") {
// Again, we can't render the component if the user isn't authorized. You
// can even put data in the response that might help the user rectify the
// issue! Like emailing the webmaster for access to the page. (Oh, right,
// `json` is just a Response helper that makes it easier to send JSON
// responses).
throw json({ webmasterEmail: "hello@remix.run" }, { status: 401 });
}
// Sometimes your code just blows up and you never anticipated it. Remix will
// automatically catch it and send the UI to the error boundary.
if (params.id === "kaboom") {
lol();
}
// but otherwise the record was found, user has access, so we can do whatever
// else we needed to in the loader and return the data. (This is boring, we're
// just gonna return the params.id).
return { param: params.id };
};
export default function ParamDemo() {
let data = useLoaderData();
return (
<h1>
The param is <i style={{ color: "red" }}>{data.param}</i>
</h1>
);
}
// https://remix.run/api/conventions#catchboundary
// https://remix.run/api/remix#usecatch
// https://remix.run/api/guides/not-found
export function CatchBoundary() {
let caught = useCatch();
let message: React.ReactNode;
switch (caught.status) {
case 401:
message = (
<p>
Looks like you tried to visit a page that you do not have access to.
Maybe ask the webmaster ({caught.data.webmasterEmail}) for access.
</p>
);
case 404:
message = (
<p>Looks like you tried to visit a page that does not exist.</p>
);
default:
message = (
<p>
There was a problem with your request!
<br />
{caught.status} {caught.statusText}
</p>
);
}
return (
<>
<h2>Oops!</h2>
<p>{message}</p>
<p>
(Isn't it cool that the user gets to stay in context and try a different
link in the parts of the UI that didn't blow up?)
</p>
</>
);
}
// https://remix.run/api/conventions#errorboundary
// https://remix.run/api/guides/not-found
export function ErrorBoundary({ error }: { error: Error }) {
console.error(error);
return (
<>
<h2>Error!</h2>
<p>{error.message}</p>
<p>
(Isn't it cool that the user gets to stay in context and try a different
link in the parts of the UI that didn't blow up?)
</p>
</>
);
}
export let meta: MetaFunction = ({ data }) => {
return {
title: data ? `Param: ${data.param}` : "Oops...",
};
};

View File

@@ -0,0 +1,40 @@
import { useCatch, Link, json, useLoaderData, Outlet } from "remix";
import type { LoaderFunction } from "remix";
export default function Boundaries() {
return (
<>
<h2>Params</h2>
<p>
When you name a route segment with $ like{" "}
<code>routes/users/$userId.js</code>, the $ segment will be parsed from
the URL and sent to your loaders and actions by the same name.
</p>
<h2>Errors</h2>
<p>
When a route throws and error in it's action, loader, or component,
Remix automatically catches it, won't even try to render the component,
but it will render the route's ErrorBoundary instead. If the route
doesn't have one, it will bubble up to the routes above it until it hits
the root.
</p>
<p>So be as granular as you want with your error handling.</p>
<h2>Not Found</h2>
<p>
(and other{" "}
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses">
client errors
</a>
)
</p>
<p>
Loaders and Actions can throw a <code>Response</code> instead of an
error and Remix will render the CatchBoundary instead of the component.
This is great when loading data from a database isn't found. As soon as
you know you can't render the component normally, throw a 404 response
and send your app into the catch boundary. Just like error boundaries,
catch boundaries bubble, too.
</p>
</>
);
}

View File

@@ -0,0 +1,100 @@
import type { MetaFunction, LoaderFunction } from "remix";
import { useLoaderData, json, Link } from "remix";
type IndexData = {
resources: Array<{ name: string; url: string }>;
demos: Array<{ name: string; to: string }>;
};
// Loaders provide data to components and are only ever called on the server, so
// you can connect to a database or run any server side code you want right next
// to the component that renders it.
// https://remix.run/api/conventions#loader
export let loader: LoaderFunction = () => {
let data: IndexData = {
resources: [
{
name: "Remix Docs",
url: "https://remix.run/docs"
},
{
name: "React Router Docs",
url: "https://reactrouter.com/docs"
},
{
name: "Remix Discord",
url: "https://discord.gg/VBePs6d"
}
],
demos: [
{
to: "demos/actions",
name: "Actions"
},
{
to: "demos/about",
name: "Nested Routes, CSS loading/unloading"
},
{
to: "demos/params",
name: "URL Params and Error Boundaries"
}
]
};
// https://remix.run/api/remix#json
return json(data);
};
// https://remix.run/api/conventions#meta
export let meta: MetaFunction = () => {
return {
title: "Remix Starter",
description: "Welcome to remix!"
};
};
// https://remix.run/guides/routing#index-routes
export default function Index() {
let data = useLoaderData<IndexData>();
return (
<div className="remix__page">
<main>
<h2>Welcome to Remix!</h2>
<p>We're stoked that you're here. 🥳</p>
<p>
Feel free to take a look around the code to see how Remix does things,
it might be a bit different than what youre used to. When you're
ready to dive deeper, we've got plenty of resources to get you
up-and-running quickly.
</p>
<p>
Check out all the demos in this starter, and then just delete the{" "}
<code>app/routes/demos</code> and <code>app/styles/demos</code>{" "}
folders when you're ready to turn this into your next project.
</p>
</main>
<aside>
<h2>Demos In This App</h2>
<ul>
{data.demos.map(demo => (
<li key={demo.to} className="remix__page__resource">
<Link to={demo.to} prefetch="intent">
{demo.name}
</Link>
</li>
))}
</ul>
<h2>Resources</h2>
<ul>
{data.resources.map(resource => (
<li key={resource.url} className="remix__page__resource">
<a href={resource.url}>{resource.name}</a>
</li>
))}
</ul>
</aside>
</div>
);
}

View File

@@ -0,0 +1,7 @@
:root {
--color-foreground: hsl(0, 0%, 100%);
--color-background: hsl(0, 0%, 7%);
--color-links: hsl(213, 100%, 73%);
--color-links-hover: hsl(213, 100%, 80%);
--color-border: hsl(0, 0%, 25%);
}

View File

@@ -0,0 +1,26 @@
/*
* Whoa whoa whoa, wait a sec...why are we overriding global CSS selectors?
* Isn't that kind of scary? How do we know this won't have side effects?
*
* In Remix, CSS that is included in a route file will *only* show up on that
* route (and for nested routes, its children). When the user navigates away
* from that route the CSS files linked from those routes will be automatically
* unloaded, making your styles much easier to predict and control.
*
* Read more about styling routes in the docs:
* https://remix.run/guides/styling
*/
:root {
--color-foreground: hsl(0, 0%, 7%);
--color-background: hsl(56, 100%, 50%);
--color-links: hsl(345, 56%, 39%);
--color-links-hover: hsl(345, 51%, 49%);
--color-border: rgb(184, 173, 20);
--font-body: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
}
.about__intro {
max-width: 500px;
}

View File

@@ -0,0 +1,216 @@
/*
* You can just delete everything here or keep whatever you like, it's just a
* quick baseline!
*/
:root {
--color-foreground: hsl(0, 0%, 7%);
--color-background: hsl(0, 0%, 100%);
--color-links: hsl(213, 100%, 52%);
--color-links-hover: hsl(213, 100%, 43%);
--color-border: hsl(0, 0%, 82%);
--font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica, Roboto,
Arial, sans-serif, system-ui, "Apple Color Emoji", "Segoe UI Emoji";
}
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
:-moz-focusring {
outline: auto;
}
:focus {
outline: var(--color-links) solid 2px;
outline-offset: 2px;
}
html,
body {
padding: 0;
margin: 0;
background-color: var(--color-background);
color: var(--color-foreground);
}
body {
font-family: var(--font-body);
line-height: 1.5;
}
a {
color: var(--color-links);
text-decoration: none;
}
a:hover {
color: var(--color-links-hover);
text-decoration: underline;
}
hr {
display: block;
height: 1px;
border: 0;
background-color: var(--color-border);
margin-top: 2rem;
margin-bottom: 2rem;
}
input:where([type="text"]),
input:where([type="search"]) {
display: block;
border: 1px solid var(--color-border);
width: 100%;
font: inherit;
line-height: 1;
height: calc(1ch + 1.5em);
padding-right: 0.5em;
padding-left: 0.5em;
background-color: hsl(0 0% 100% / 20%);
color: var(--color-foreground);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.container {
--gutter: 16px;
width: 1024px;
max-width: calc(100% - var(--gutter) * 2);
margin-right: 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

@@ -0,0 +1,29 @@
{
"private": true,
"name": "remix-app-template",
"description": "",
"license": "",
"scripts": {
"build": "remix build",
"dev": "remix dev",
"postinstall": "remix setup node"
},
"dependencies": {
"@remix-run/react": "^1.0.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"remix": "^1.0.6",
"@remix-run/serve": "^1.0.6",
"@remix-run/vercel": "^1.0.6"
},
"devDependencies": {
"@remix-run/dev": "^1.0.6",
"@types/react": "^17.0.24",
"@types/react-dom": "^17.0.9",
"typescript": "^4.1.2"
},
"engines": {
"node": ">=14"
},
"sideEffects": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,9 @@
/**
* @type {import('@remix-run/dev/config').AppConfig}
*/
module.exports = {
appDirectory: "app",
browserBuildDirectory: "public/build",
publicPath: "/build/",
serverBuildDirectory: "api/_build"
};

2
examples/remix/remix.env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node/globals" />

View File

@@ -0,0 +1,18 @@
{
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"resolveJsonModule": true,
"target": "ES2019",
"strict": true,
"paths": {
"~/*": ["./app/*"]
},
// Remix takes care of building everything in `remix build`.
"noEmit": true
}
}

View File

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

View File

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

View File

@@ -1,65 +1,190 @@
import fs from 'fs-extra';
import { join, dirname, relative } from 'path';
import { join, dirname, parse, relative } from 'path';
import glob from './fs/glob';
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 { BuilderFunctions, BuildOptions, Files } from './types';
import minimatch from 'minimatch';
import type { BuildOptions, Files } from './types';
import { getIgnoreFilter } from '.';
// `.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.
* @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`
*/
export function convertRuntimeToPlugin(
buildRuntime: (options: BuildOptions) => Promise<{ output: Lambda }>,
packageName: string,
ext: string
) {
// This `build()` signature should match `plugin.build()` signature in `vercel build`.
return async function build({ workPath }: { workPath: string }) {
const opts = { cwd: workPath };
const files = await glob('**', opts);
delete files['vercel.json']; // Builders/Runtimes didn't have vercel.json
const entrypoints = await glob(`api/**/*${ext}`, opts);
// We also don't want to provide any files to Runtimes that were ignored
// through `.vercelignore` or `.nowignore`, because the Build Step does the same.
const ignoreFilter = await getIgnoreFilter(workPath);
// Retrieve the files that are currently available on the File System,
// before the Legacy Runtime has even started to build.
const sourceFilesPreBuild = await getSourceFiles(workPath, ignoreFilter);
// Instead of doing another `glob` to get all the matching source files,
// 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 { functions = {} } = await readVercelConfig(workPath);
const traceDir = join(workPath, '.output', 'runtime-traced-files');
const pluginName = packageName.replace('vercel-plugin-', '');
const traceDir = join(
workPath,
`.output`,
`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);
for (const entrypoint of Object.keys(entrypoints)) {
const key =
Object.keys(functions).find(
src => src === entrypoint || minimatch(entrypoint, src)
) || '';
const config = functions[key] || {};
const { output } = await buildRuntime({
files,
files: sourceFilesPreBuild,
entrypoint,
workPath,
config: {
zeroConfig: true,
includeFiles: config.includeFiles,
excludeFiles: config.excludeFiles,
},
meta: {
avoidTopLevelInstall: true,
},
});
// Legacy Runtimes tend to pollute the `workPath` with compiled results,
// because the `workPath` used to be a place that was a place where they could
// just put anything, but nowadays it's the working directory of the `vercel build`
// command, which is the place where the developer keeps their source files,
// so we don't want to pollute this space unnecessarily. That means we have to clean
// up files that were created by the build, which is done further below.
const sourceFilesAfterBuild = await getSourceFiles(
workPath,
ignoreFilter
);
// Further down, we will need the filename of the Lambda handler
// for placing it inside `server/pages/api`, but because Legacy Runtimes
// don't expose the filename directly, we have to construct it
// from the handler name, and then find the matching file further below,
// because we don't yet know its extension here.
const handler = output.handler;
const handlerMethod = handler.split('.').reverse()[0];
const handlerFileName = handler.replace(`.${handlerMethod}`, '');
pages[entrypoint] = {
handler: output.handler,
handler: handler,
runtime: output.runtime,
memory: output.memory,
maxDuration: output.maxDuration,
environment: output.environment,
allowQuery: output.allowQuery,
regions: output.regions,
};
// @ts-ignore This symbol is a private API
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];
}
}
const handlerFilePath = Object.keys(lambdaFiles).find(item => {
return parse(item).name === handlerFileName;
});
const handlerFileOrigin = lambdaFiles[handlerFilePath || ''].fsPath;
if (!handlerFileOrigin) {
throw new Error(
`Could not find a handler file. Please ensure that the list of \`files\` defined for the returned \`Lambda\` contains a file with the name ${handlerFileName} (+ any extension).`
);
}
const entry = join(workPath, '.output', 'server', 'pages', entrypoint);
await fs.ensureDir(dirname(entry));
await linkOrCopy(files[entrypoint].fsPath, entry);
await linkOrCopy(handlerFileOrigin, entry);
const toRemove = [];
// You can find more details about this at the point where the
// `sourceFilesAfterBuild` is created originally.
for (const file in sourceFilesAfterBuild) {
if (!sourceFilesPreBuild[file]) {
const path = sourceFilesAfterBuild[file].fsPath;
toRemove.push(fs.remove(path));
}
}
await Promise.all(toRemove);
const tracedFiles: {
absolutePath: string;
@@ -68,7 +193,14 @@ export function convertRuntimeToPlugin(
Object.entries(lambdaFiles).forEach(async ([relPath, file]) => {
const newPath = join(traceDir, relPath);
// The handler was already moved into position above.
if (relPath === handlerFilePath) {
return;
}
tracedFiles.push({ absolutePath: newPath, relativePath: relPath });
if (file.fsPath) {
await linkOrCopy(file.fsPath, newPath);
} else if (file.type === 'FileBlob') {
@@ -86,11 +218,12 @@ export function convertRuntimeToPlugin(
'pages',
`${entrypoint}.nft.json`
);
const json = JSON.stringify({
version: 1,
files: tracedFiles.map(f => ({
input: normalizePath(relative(nft, f.absolutePath)),
output: normalizePath(f.relativePath),
files: tracedFiles.map(file => ({
input: normalizePath(relative(nft, file.absolutePath)),
output: normalizePath(file.relativePath),
})),
});
@@ -124,17 +257,9 @@ async function readJson(filePath: string): Promise<{ [key: string]: any }> {
}
}
async function readVercelConfig(
workPath: string
): Promise<{ functions?: BuilderFunctions; regions?: string[] }> {
const vercelJsonPath = join(workPath, 'vercel.json');
return readJson(vercelJsonPath);
}
/**
* If `.output/functions-manifest.json` exists, append to the pages
* property. Otherwise write a new file. This will also read `vercel.json`
* and apply relevant `functions` property config.
* property. Otherwise write a new file.
*/
export async function updateFunctionsManifest({
workPath,
@@ -148,55 +273,94 @@ export async function updateFunctionsManifest({
'.output',
'functions-manifest.json'
);
const vercelConfig = await readVercelConfig(workPath);
const functionsManifest = await readJson(functionsManifestPath);
if (!functionsManifest.version) functionsManifest.version = 1;
if (!functionsManifest.pages) functionsManifest.pages = {};
for (const [pageKey, pageConfig] of Object.entries(pages)) {
const fnConfig = await getLambdaOptionsFromFunction({
sourceFile: pageKey,
config: vercelConfig,
});
functionsManifest.pages[pageKey] = {
...pageConfig,
memory: fnConfig.memory || pageConfig.memory,
maxDuration: fnConfig.maxDuration || pageConfig.maxDuration,
regions: vercelConfig.regions || pageConfig.regions,
};
functionsManifest.pages[pageKey] = { ...pageConfig };
}
await fs.writeFile(functionsManifestPath, JSON.stringify(functionsManifest));
}
/**
* Will append routes to the `routes-manifest.json` file.
* If the file does not exist, it'll be created.
* Append routes to the `routes-manifest.json` file.
* If the file does not exist, it will be created.
*/
export async function updateRoutesManifest({
workPath,
redirects,
rewrites,
headers,
dynamicRoutes,
staticRoutes,
}: {
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?: {
page: string;
regex: string;
namedRegex?: 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 routesManifest = await readJson(routesManifestPath);
if (!routesManifest.version) routesManifest.version = 1;
if (!routesManifest.version) routesManifest.version = 3;
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 (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = [];
routesManifest.dynamicRoutes.push(...dynamicRoutes);
}
if (staticRoutes) {
if (!routesManifest.staticRoutes) routesManifest.staticRoutes = [];
routesManifest.staticRoutes.push(...staticRoutes);
}
await fs.writeFile(routesManifestPath, JSON.stringify(routesManifest));
}

View File

@@ -96,6 +96,7 @@ export async function detectBuilders(
redirectRoutes: Route[] | null;
rewriteRoutes: Route[] | null;
errorRoutes: Route[] | null;
limitedRoutes: LimitedRoutes | null;
}> {
const errors: ErrorResponse[] = [];
const warnings: ErrorResponse[] = [];
@@ -114,6 +115,7 @@ export async function detectBuilders(
redirectRoutes: null,
rewriteRoutes: null,
errorRoutes: null,
limitedRoutes: null,
};
}
@@ -179,6 +181,7 @@ export async function detectBuilders(
redirectRoutes: null,
rewriteRoutes: null,
errorRoutes: null,
limitedRoutes: null,
};
}
@@ -257,6 +260,7 @@ export async function detectBuilders(
defaultRoutes: null,
rewriteRoutes: null,
errorRoutes: null,
limitedRoutes: null,
};
}
@@ -299,6 +303,7 @@ export async function detectBuilders(
defaultRoutes: null,
rewriteRoutes: null,
errorRoutes: null,
limitedRoutes: null,
};
}
@@ -326,6 +331,7 @@ export async function detectBuilders(
}
const routesResult = getRouteResult(
pkg,
apiRoutes,
dynamicRoutes,
usedOutputDirectory,
@@ -342,6 +348,7 @@ export async function detectBuilders(
defaultRoutes: routesResult.defaultRoutes,
rewriteRoutes: routesResult.rewriteRoutes,
errorRoutes: routesResult.errorRoutes,
limitedRoutes: routesResult.limitedRoutes,
};
}
@@ -932,7 +939,14 @@ function createRouteFromPath(
return { route, isDynamic };
}
interface LimitedRoutes {
defaultRoutes: Route[];
redirectRoutes: Route[];
rewriteRoutes: Route[];
}
function getRouteResult(
pkg: PackageJson | undefined | null,
apiRoutes: Source[],
dynamicRoutes: Source[],
outputDirectory: string,
@@ -944,11 +958,18 @@ function getRouteResult(
redirectRoutes: Route[];
rewriteRoutes: Route[];
errorRoutes: Route[];
limitedRoutes: LimitedRoutes;
} {
const deps = Object.assign({}, pkg?.dependencies, pkg?.devDependencies);
const defaultRoutes: Route[] = [];
const redirectRoutes: Route[] = [];
const rewriteRoutes: Route[] = [];
const errorRoutes: Route[] = [];
const limitedRoutes: LimitedRoutes = {
defaultRoutes: [],
redirectRoutes: [],
rewriteRoutes: [],
};
const framework = frontendBuilder?.config?.framework || '';
const isNextjs =
framework === 'nextjs' || isOfficialRuntime('next', frontendBuilder?.use);
@@ -956,14 +977,43 @@ function getRouteResult(
if (apiRoutes && apiRoutes.length > 0) {
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 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) {
const exts = Array.from(extSet)
const extGroup = `(?:\\.(?:${Array.from(extSet)
.map(ext => ext.slice(1))
.join('|');
const extGroup = `(?:\\.(?:${exts}))`;
.join('|')}))`;
const extGroupLimited = `(?:\\.(?:${Array.from(extSetLimited)
.map(ext => ext.slice(1))
.join('|')}))`;
if (options.cleanUrls) {
redirectRoutes.push({
@@ -979,6 +1029,20 @@ function getRouteResult(
},
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 {
defaultRoutes.push({ handle: 'miss' });
defaultRoutes.push({
@@ -986,10 +1050,18 @@ function getRouteResult(
dest: '/api/$1',
check: true,
});
limitedRoutes.defaultRoutes.push({ handle: 'miss' });
limitedRoutes.defaultRoutes.push({
src: `^/api/(.+)${extGroupLimited}$`,
dest: '/api/$1',
check: true,
});
}
}
rewriteRoutes.push(...dynamicRoutes);
limitedRoutes.rewriteRoutes.push(...dynamicRoutes);
if (typeof ignoreRuntimes === 'undefined') {
// This route is only necessary to hide the directory listing
@@ -1040,6 +1112,7 @@ function getRouteResult(
redirectRoutes,
rewriteRoutes,
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 FileFsRef from './file-fs-ref';
import FileRef from './file-ref';
@@ -33,6 +34,7 @@ import { NowBuildError } from './errors';
import streamToBuffer from './fs/stream-to-buffer';
import shouldServe from './should-serve';
import debug from './debug';
import getIgnoreFilter from './get-ignore-filter';
export {
FileBlob,
@@ -70,6 +72,7 @@ export {
isSymbolicLink,
getLambdaOptionsFromFunction,
scanParentDirs,
getIgnoreFilter,
};
export {
@@ -132,3 +135,11 @@ export const getPlatformEnv = (name: string): string | undefined => {
}
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[];
env?: Env;
buildEnv?: Env;
avoidTopLevelInstall?: boolean;
}
export interface AnalyzeOptions {

View File

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

View File

@@ -1,9 +1,9 @@
{
"functions": {
"api/users/post.py": {
"api/users.rb": {
"memory": 3008
},
"api/not-matching-anything.py": {
"api/doesnt-exist.rb": {
"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 { defaultRoutes, rewriteRoutes, errorRoutes } = await detectBuilders(
files,
null,
{
const { defaultRoutes, rewriteRoutes, errorRoutes, limitedRoutes } =
await detectBuilders(files, null, {
featHandleMiss,
}
);
});
expect(defaultRoutes).toStrictEqual([
{ 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!);
[
@@ -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 { defaultRoutes, redirectRoutes, rewriteRoutes, errorRoutes } =
await detectBuilders(files, null, options);
const {
defaultRoutes,
redirectRoutes,
rewriteRoutes,
errorRoutes,
limitedRoutes,
} = await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
expect(defaultRoutes).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
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 { defaultRoutes, redirectRoutes, rewriteRoutes } =
const { defaultRoutes, redirectRoutes, rewriteRoutes, limitedRoutes } =
await detectBuilders(files, null, options);
testHeaders(redirectRoutes);
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
const getLocation = createReplaceLocation(redirectRoutes);

View File

@@ -1,6 +1,6 @@
import { join } from 'path';
import fs from 'fs-extra';
import { BuildOptions, createLambda } from '../src';
import { BuildOptions, createLambda, FileFsRef } from '../src';
import { convertRuntimeToPlugin } from '../src/convert-runtime-to-plugin';
async function fsToJson(dir: string, output: Record<string, any> = {}) {
@@ -18,24 +18,43 @@ async function fsToJson(dir: string, output: Record<string, any> = {}) {
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', () => {
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 () => {
const ext = '.py';
const workPath = pythonApiWorkpath;
const handlerName = 'vc__handler__python';
const handlerFileName = handlerName + ext;
const lambdaOptions = {
handler: 'index.handler',
handler: `${handlerName}.vc_handler`,
runtime: 'python3.9',
memory: 512,
maxDuration: 5,
environment: {},
regions: ['sfo1'],
};
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({
files: opts.files,
...lambdaOptions,
@@ -44,24 +63,30 @@ describe('convert-runtime-to-plugin', () => {
};
const lambdaFiles = await fsToJson(workPath);
delete lambdaFiles['vercel.json'];
const build = await convertRuntimeToPlugin(buildRuntime, '.py');
const packageName = 'vercel-plugin-python';
const build = await convertRuntimeToPlugin(buildRuntime, packageName, ext);
await build({ workPath });
const output = await fsToJson(join(workPath, '.output'));
delete lambdaFiles['vercel.json'];
delete lambdaFiles['vc__handler__python.py'];
expect(output).toMatchObject({
'functions-manifest.json': expect.stringContaining('{'),
'runtime-traced-files': lambdaFiles,
inputs: {
'api-routes-python': lambdaFiles,
},
server: {
pages: {
api: {
'index.py': expect.stringContaining('index'),
'index.py': expect.stringContaining('handler'),
'index.py.nft.json': expect.stringContaining('{'),
users: {
'get.py': expect.stringContaining('get'),
'get.py': expect.stringContaining('handler'),
'get.py.nft.json': expect.stringContaining('{'),
'post.py': expect.stringContaining('post'),
'post.py': expect.stringContaining('handler'),
'post.py.nft.json': expect.stringContaining('{'),
},
},
@@ -75,7 +100,7 @@ describe('convert-runtime-to-plugin', () => {
pages: {
'api/index.py': lambdaOptions,
'api/users/get.py': lambdaOptions,
'api/users/post.py': { ...lambdaOptions, memory: 3008 },
'api/users/post.py': { ...lambdaOptions, memory: 512 },
},
});
@@ -84,27 +109,35 @@ describe('convert-runtime-to-plugin', () => {
version: 1,
files: [
{
input: '../../../../runtime-traced-files/api/index.py',
input: `../../../../inputs/api-routes-python/api/db/[id].py`,
output: 'api/db/[id].py',
},
{
input: `../../../../inputs/api-routes-python/api/index.py`,
output: 'api/index.py',
},
{
input: '../../../../runtime-traced-files/api/users/get.py',
input: `../../../../inputs/api-routes-python/api/project/[aid]/[bid]/index.py`,
output: 'api/project/[aid]/[bid]/index.py',
},
{
input: `../../../../inputs/api-routes-python/api/users/get.py`,
output: 'api/users/get.py',
},
{
input: '../../../../runtime-traced-files/api/users/post.py',
input: `../../../../inputs/api-routes-python/api/users/post.py`,
output: 'api/users/post.py',
},
{
input: '../../../../runtime-traced-files/file.txt',
input: `../../../../inputs/api-routes-python/file.txt`,
output: 'file.txt',
},
{
input: '../../../../runtime-traced-files/util/date.py',
input: `../../../../inputs/api-routes-python/util/date.py`,
output: 'util/date.py',
},
{
input: '../../../../runtime-traced-files/util/math.py',
input: `../../../../inputs/api-routes-python/util/math.py`,
output: 'util/math.py',
},
],
@@ -117,27 +150,35 @@ describe('convert-runtime-to-plugin', () => {
version: 1,
files: [
{
input: '../../../../../runtime-traced-files/api/index.py',
input: `../../../../../inputs/api-routes-python/api/db/[id].py`,
output: 'api/db/[id].py',
},
{
input: `../../../../../inputs/api-routes-python/api/index.py`,
output: 'api/index.py',
},
{
input: '../../../../../runtime-traced-files/api/users/get.py',
input: `../../../../../inputs/api-routes-python/api/project/[aid]/[bid]/index.py`,
output: 'api/project/[aid]/[bid]/index.py',
},
{
input: `../../../../../inputs/api-routes-python/api/users/get.py`,
output: 'api/users/get.py',
},
{
input: '../../../../../runtime-traced-files/api/users/post.py',
input: `../../../../../inputs/api-routes-python/api/users/post.py`,
output: 'api/users/post.py',
},
{
input: '../../../../../runtime-traced-files/file.txt',
input: `../../../../../inputs/api-routes-python/file.txt`,
output: 'file.txt',
},
{
input: '../../../../../runtime-traced-files/util/date.py',
input: `../../../../../inputs/api-routes-python/util/date.py`,
output: 'util/date.py',
},
{
input: '../../../../../runtime-traced-files/util/math.py',
input: `../../../../../inputs/api-routes-python/util/math.py`,
output: 'util/math.py',
},
],
@@ -150,27 +191,35 @@ describe('convert-runtime-to-plugin', () => {
version: 1,
files: [
{
input: '../../../../../runtime-traced-files/api/index.py',
input: `../../../../../inputs/api-routes-python/api/db/[id].py`,
output: 'api/db/[id].py',
},
{
input: `../../../../../inputs/api-routes-python/api/index.py`,
output: 'api/index.py',
},
{
input: '../../../../../runtime-traced-files/api/users/get.py',
input: `../../../../../inputs/api-routes-python/api/project/[aid]/[bid]/index.py`,
output: 'api/project/[aid]/[bid]/index.py',
},
{
input: `../../../../../inputs/api-routes-python/api/users/get.py`,
output: 'api/users/get.py',
},
{
input: '../../../../../runtime-traced-files/api/users/post.py',
input: `../../../../../inputs/api-routes-python/api/users/post.py`,
output: 'api/users/post.py',
},
{
input: '../../../../../runtime-traced-files/file.txt',
input: `../../../../../inputs/api-routes-python/file.txt`,
output: 'file.txt',
},
{
input: '../../../../../runtime-traced-files/util/date.py',
input: `../../../../../inputs/api-routes-python/util/date.py`,
output: 'util/date.py',
},
{
input: '../../../../../runtime-traced-files/util/math.py',
input: `../../../../../inputs/api-routes-python/util/math.py`,
output: 'util/math.py',
},
],

View File

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

View File

@@ -14,9 +14,10 @@ import { assert } from 'console';
import { createHash } from 'crypto';
import fs from 'fs-extra';
import ogGlob from 'glob';
import { isAbsolute, join, parse, relative, resolve } from 'path';
import { dirname, isAbsolute, join, parse, relative, resolve } from 'path';
import pluralize from 'pluralize';
import Client from '../util/client';
import { VercelConfig } from '../util/dev/types';
import { emoji, prependEmoji } from '../util/emoji';
import getArgs from '../util/get-args';
import handleError from '../util/handle-error';
@@ -135,9 +136,11 @@ export default async function main(client: Client) {
});
// 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' },
};
@@ -283,6 +286,21 @@ export default async function main(client: Client) {
// Clean the output directory
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
// "node-modules" linker style
const env = {
@@ -316,18 +334,40 @@ export default async function main(client: Client) {
}
if (!fs.existsSync(join(cwd, OUTPUT_DIR))) {
let outputDir = join(OUTPUT_DIR, 'static');
let distDir = await framework.getFsOutputDir(cwd);
if (framework.slug === 'nextjs') {
outputDir = OUTPUT_DIR;
let dotNextDir: string | null = null;
// If a custom `outputDirectory` was set, we'll need to verify
// 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 differnt project that's not in the settings.
const isNextOutput = Boolean(dotNextDir);
const outputDir = isNextOutput ? OUTPUT_DIR : join(OUTPUT_DIR, 'static');
const distDir =
dotNextDir ||
userOutputDirectory ||
(await framework.getFsOutputDir(cwd));
await fs.ensureDir(join(cwd, outputDir));
const relativeDistDir = relative(cwd, distDir);
const copyStamp = stamp();
client.output.spinner(
`Copying files from ${param(distDir)} to ${param(outputDir)}`
);
const files = await glob(join(relativeDistDir, '**'), {
const files = await glob(join(relative(cwd, distDir), '**'), {
ignore: [
'node_modules/**',
'.vercel/**',
@@ -375,6 +415,7 @@ export default async function main(client: Client) {
`Generating build manifest: ${param(buildManifestPath)}`
);
const buildManifest = {
version: 1,
cache: framework.cachePattern ? [framework.cachePattern] : [],
};
await fs.writeJSON(buildManifestPath, buildManifest, { spaces: 2 });
@@ -402,7 +443,7 @@ export default async function main(client: Client) {
}
// Special Next.js processing.
if (framework.slug === 'nextjs') {
if (isNextOutput) {
// The contents of `.output/static` should be placed inside of `.output/static/_next/static`
const tempStatic = '___static';
await fs.rename(
@@ -453,10 +494,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
// 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).
const nextSrcDirectory = dirname(distDir);
const publicFiles = await glob('public/**', {
nodir: true,
dot: true,
cwd,
cwd: nextSrcDirectory,
absolute: true,
});
if (publicFiles.length > 0) {
@@ -465,7 +508,11 @@ export default async function main(client: Client) {
smartCopy(
client,
f,
f.replace('public', join(OUTPUT_DIR, 'static'))
join(
OUTPUT_DIR,
'static',
relative(join(dirname(distDir), 'public'), f)
)
)
)
);
@@ -473,7 +520,7 @@ export default async function main(client: Client) {
const staticFiles = await glob('static/**', {
nodir: true,
dot: true,
cwd,
cwd: nextSrcDirectory,
absolute: true,
});
await Promise.all(
@@ -481,7 +528,12 @@ export default async function main(client: Client) {
smartCopy(
client,
f,
f.replace('static', join(OUTPUT_DIR, 'static', 'static'))
join(
OUTPUT_DIR,
'static',
'static',
relative(join(dirname(distDir), 'static'), f)
)
)
)
);
@@ -500,6 +552,7 @@ export default async function main(client: Client) {
const nftFiles = await glob(join(OUTPUT_DIR, '**', '*.nft.json'), {
nodir: true,
dot: true,
ignore: ['cache/**'],
cwd,
absolute: true,
});
@@ -536,6 +589,7 @@ export default async function main(client: Client) {
baseDir,
outputDir: OUTPUT_DIR,
nftFileName: f.replace(ext, '.js.nft.json'),
distDir,
nft: {
version: 1,
files: Array.from(fileList).map(fileListEntry =>
@@ -553,6 +607,7 @@ export default async function main(client: Client) {
outputDir: OUTPUT_DIR,
nftFileName: f,
nft: json,
distDir,
});
}
}
@@ -561,22 +616,33 @@ export default async function main(client: Client) {
OUTPUT_DIR,
'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 {
input: i.replace('.next', '.output'),
output,
};
}),
});
if (fs.existsSync(requiredServerFilesPath)) {
client.output.debug(`Resolve ${param('required-server-files.json')}.`);
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));
const absolutePath = join(cwd, relPath);
const output = relative(baseDir, absolutePath);
return relPath === output
? relPath
: {
input: relPath,
output,
};
}),
});
}
}
}
@@ -588,6 +654,14 @@ export default async function main(client: Client) {
plugins.pluginCount
)} after Build Command:`
);
let vercelConfig: VercelConfig = {};
try {
vercelConfig = await fs.readJSON(join(cwd, 'vercel.json'));
} catch (error) {
if (error.code !== 'ENOENT') {
throw new Error(`Failed to read vercel.json: ${error.message}`);
}
}
for (let item of plugins.buildPlugins) {
const { name, plugin, color } = item;
if (typeof plugin.build === 'function') {
@@ -600,6 +674,7 @@ export default async function main(client: Client) {
console.error = (...args: any[]) =>
prefixedLog(prefix, args, origErr);
await plugin.build({
vercelConfig,
workPath: cwd,
});
client.output.debug(
@@ -746,24 +821,37 @@ async function resolveNftToOutput({
baseDir,
outputDir,
nftFileName,
distDir,
nft,
}: {
client: Client;
baseDir: string;
outputDir: string;
nftFileName: string;
distDir: string;
nft: NftFile;
}) {
client.output.debug(`Processing and resolving ${nftFileName}`);
await fs.ensureDir(join(outputDir, 'inputs'));
const newFilesList: NftFile['files'] = [];
// If `distDir` is a subdirectory, then the input has to be resolved to where the `.output` directory will be.
const relNftFileName = relative(outputDir, nftFileName);
const origNftFilename = join(distDir, relNftFileName);
if (relNftFileName.startsWith('cache/')) {
// No need to process the `cache/` directory.
// Paths in it might also not be relative to `cache` itself.
return;
}
for (let fileEntity of nft.files) {
const relativeInput: string =
const relativeInput =
typeof fileEntity === 'string' ? fileEntity : fileEntity.input;
const fullInput = resolve(join(parse(nftFileName).dir, relativeInput));
const fullInput = resolve(join(parse(origNftFilename).dir, relativeInput));
// if the resolved path is NOT in the .output directory we move in it there
if (!fullInput.includes(outputDir)) {
if (!fullInput.includes(distDir)) {
const { ext } = parse(fullInput);
const raw = await fs.readFile(fullInput);
const newFilePath = join(outputDir, 'inputs', hash(raw) + ext);

View File

@@ -968,7 +968,7 @@ export default class DevServer {
socket.destroy();
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.proxy.ws(req, socket, head, { target });
});
@@ -1663,7 +1663,7 @@ export default class DevServer {
if (!match) {
// If the dev command is started, then proxy to it
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}`);
// Add the Vercel platform proxy request headers
@@ -1810,7 +1810,7 @@ export default class DevServer {
return proxyPass(
req,
res,
`http://localhost:${port}`,
`http://127.0.0.1:${port}`,
this,
requestId,
false
@@ -1847,7 +1847,7 @@ export default class DevServer {
return proxyPass(
req,
res,
`http://localhost:${this.devProcessPort}`,
`http://127.0.0.1:${this.devProcessPort}`,
this,
requestId,
false

View File

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

View File

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

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

@@ -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,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "0.5.1-canary.12",
"version": "0.5.1-canary.16",
"main": "./dist/frameworks.js",
"types": "./dist/frameworks.d.ts",
"files": [

View File

@@ -112,7 +112,7 @@ export const frameworks = [
description:
'A Gatsby app, using the default starter theme and a Serverless Function API.',
website: 'https://gatsbyjs.org',
sort: 2,
sort: 5,
envPrefix: 'GATSBY_',
detectors: {
every: [
@@ -191,6 +191,74 @@ export const frameworks = [
},
cachePattern: '{.cache,public}/**',
},
{
name: 'Remix',
slug: 'remix',
demo: 'https://remix.examples.vercel.com',
logo: 'https://raw.githubusercontent.com/vercel/vercel/main/packages/frameworks/logos/remix-no-shadow.svg',
tagline: 'Build Better Websites',
description: 'A new Remix app — the result of running `npx create-remix`.',
website: 'https://remix.run',
sort: 6,
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"remix":\\s*".+?"[^}]*}',
},
],
},
settings: {
installCommand: {
placeholder: '`yarn install` or `npm install`',
},
buildCommand: {
value: 'remix build',
placeholder: '`npm run build` or `remix build`',
},
devCommand: {
value: 'remix dev',
placeholder: 'remix dev',
},
outputDirectory: {
value: 'public',
},
},
dependency: 'remix',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
defaultRoutes: [
{
src: '^/build/(.*)$',
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
continue: true,
},
{
handle: 'filesystem',
},
{
src: '/(.*)',
dest: '/api',
},
],
defaultRewrites: [
{
source: '/(.*)',
regex: '/(.*)',
destination: '/api',
},
],
defaultHeaders: [
{
source: '/build/(.*)',
regex: '/build/(.*)',
headers: [
{ key: 'cache-control', value: 'public, max-age=31536000, immutable' },
],
},
]
},
{
name: 'Hexo',
slug: 'hexo',
@@ -200,7 +268,6 @@ export const frameworks = [
'Hexo is a fast, simple & powerful blog framework powered by Node.js.',
description: 'A Hexo site, created with the Hexo CLI.',
website: 'https://hexo.io',
sort: 3,
detectors: {
every: [
{
@@ -239,7 +306,6 @@ export const frameworks = [
'11ty is a simpler static site generator written in JavaScript, created to be an alternative to Jekyll.',
description: 'An Eleventy site, created with npm init.',
website: 'https://www.11ty.dev',
sort: 4,
detectors: {
every: [
{
@@ -981,6 +1047,7 @@ export const frameworks = [
'Svelte lets you write high performance reactive apps with significantly less boilerplate.',
description: 'A basic Svelte app using the default template.',
website: 'https://svelte.dev',
sort: 3,
detectors: {
every: [
{
@@ -1177,6 +1244,7 @@ export const frameworks = [
description:
'A React app, bootstrapped with create-react-app, and a Serverless Function API.',
website: 'https://create-react-app.dev',
sort: 4,
envPrefix: 'REACT_APP_',
detectors: {
some: [
@@ -1571,6 +1639,7 @@ export const frameworks = [
'Nuxt.js is the web comprehensive framework that lets you dream big with Vue.js.',
description: 'A Nuxt.js app, bootstrapped with create-nuxt-app.',
website: 'https://nuxtjs.org',
sort: 2,
envPrefix: 'NUXT_ENV_',
detectors: {
every: [
@@ -1667,7 +1736,6 @@ export const frameworks = [
'Hugo is the worlds fastest framework for building websites, written in Go.',
description: 'A Hugo site, created with the Hugo CLI.',
website: 'https://gohugo.io',
sort: 5,
detectors: {
some: [
{

View File

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

View File

@@ -1,6 +1,6 @@
import { convertRuntimeToPlugin } from '@vercel/build-utils';
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;

View File

@@ -1,6 +1,6 @@
{
"name": "vercel-plugin-node",
"version": "1.12.2-canary.11",
"version": "1.12.2-canary.23",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -34,12 +34,12 @@
"@types/node-fetch": "2",
"@types/test-listen": "1.1.0",
"@types/yazl": "2.4.2",
"@vercel/build-utils": "2.12.3-canary.20",
"@vercel/build-utils": "2.12.3-canary.31",
"@vercel/fun": "1.0.3",
"@vercel/ncc": "0.24.0",
"@vercel/nft": "0.14.0",
"@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",
"content-type": "1.0.4",
"cookie": "0.4.0",

View File

@@ -40,6 +40,7 @@ import {
walkParentDirs,
normalizePath,
runPackageJsonScript,
getInputHash,
} from '@vercel/build-utils';
import { FromSchema } from 'json-schema-to-ts';
import { getConfig, BaseFunctionConfigSchema } from '@vercel/static-config';
@@ -47,7 +48,6 @@ import { AbortController } from 'abort-controller';
import { Register, register } from './typescript';
import { pageToRoute } from './router/page-to-route';
import { isDynamicRoute } from './router/is-dynamic';
import crypto from 'crypto';
export { shouldServe };
export {
@@ -401,13 +401,18 @@ export async function build({ workPath }: { workPath: string }) {
getConfig(project, absEntrypoint, FunctionConfigSchema) || {};
// 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.
if (config.runtime && config.runtime !== 'node') {
if (config.use && config.use !== 'node') {
continue;
}
await buildEntrypoint({ workPath, entrypoint, config, installedPaths });
await buildEntrypoint({
workPath,
entrypoint,
config,
installedPaths,
});
}
}
@@ -423,10 +428,7 @@ export async function buildEntrypoint({
installedPaths?: Set<string>;
}) {
// Unique hash that will be used as directory name for `.output`.
const entrypointHash = crypto
.createHash('sha256')
.update(entrypoint)
.digest('hex');
const entrypointHash = 'api-routes-node-' + getInputHash(entrypoint);
const outputDirPath = join(workPath, '.output');
const { dir, name } = parsePath(entrypoint);

View File

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

View File

@@ -1,6 +1,6 @@
import { convertRuntimeToPlugin } from '@vercel/build-utils';
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;

View File

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

View File

@@ -1,6 +1,6 @@
import { convertRuntimeToPlugin } from '@vercel/build-utils';
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;

View File

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

View File

@@ -114,10 +114,24 @@ export const build = async ({
try {
const json = await readFile(join(pipfileLockDir, 'Pipfile.lock'), 'utf8');
const obj = JSON.parse(json);
if (!meta.isDev && obj?._meta?.requires?.python_version === '3.6') {
pipPath = 'pip3.6';
pythonPath = 'python3.6';
pythonRuntime = 'python3.6';
const version = obj?._meta?.requires?.python_version;
if (!meta.isDev) {
if (version === '3.6') {
pipPath = 'pip3.6';
pythonPath = 'python3.6';
pythonRuntime = 'python3.6';
console.warn(
`Warning: Python version "${version}" detected in Pipfile.lock will reach End-Of-Life December 2021. Please upgrade. http://vercel.link/python-version`
);
} else if (version === '3.9') {
pipPath = 'pip3.9';
pythonPath = 'python3.9';
pythonRuntime = 'python3.9';
} else {
console.warn(
`Warning: Invalid Python version "${version}" detected in Pipfile.lock will be ignored. http://vercel.link/python-version`
);
}
}
} catch (err) {
throw new NowBuildError({

View File

@@ -1,3 +1,4 @@
import { relative, basename } from 'path';
import execa from 'execa';
import { Meta, debug } from '@vercel/build-utils';
@@ -135,6 +136,18 @@ export async function installRequirementsFile({
meta,
args = [],
}: InstallRequirementsFileArg) {
const fileAtRoot = relative(workPath, filePath) === basename(filePath);
// If the `requirements.txt` file is located in the Root Directory of the project and
// the new File System API is used (`avoidTopLevelInstall`), the Install Command
// will have already installed its dependencies, so we don't need to do it again.
if (meta.avoidTopLevelInstall && fileAtRoot) {
debug(
`Skipping requirements file installation, already installed by Install Command`
);
return;
}
if (
meta.isDev &&
(await areRequirementsInstalled(pythonPath, filePath, workPath))

View File

@@ -1,5 +0,0 @@
{
"version": 2,
"builds": [{ "src": "index.py", "use": "@vercel/python" }],
"probes": [{ "path": "/", "mustContain": "wsgi:RANDOMNESS_PLACEHOLDER" }]
}

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [{ "src": "index.py", "use": "@vercel/python" }],
"probes": [
{
"path": "/",
"mustContain": "wsgi:RANDOMNESS_PLACEHOLDER",
"logMustContain": "Warning: Python version \"3.6\" detected in Pipfile.lock will reach End-Of-Life December 2021. Please upgrade."
}
]
}

View File

@@ -1,8 +0,0 @@
{
"version": 2,
"builds": [{ "src": "index.py", "use": "@vercel/python" }],
"env": {
"RANDOMNESS": "RANDOMNESS_PLACEHOLDER"
},
"probes": [{ "path": "/", "mustContain": "RANDOMNESS_PLACEHOLDER:env" }]
}

View File

@@ -0,0 +1,14 @@
{
"version": 2,
"builds": [{ "src": "index.py", "use": "@vercel/python" }],
"env": {
"RANDOMNESS": "RANDOMNESS_PLACEHOLDER"
},
"probes": [
{
"path": "/",
"mustContain": "RANDOMNESS_PLACEHOLDER:env",
"logMustContain": "Warning: Python version \"3.6\" detected in Pipfile.lock will reach End-Of-Life December 2021. Please upgrade."
}
]
}

View File

@@ -1,5 +0,0 @@
{
"version": 2,
"builds": [{ "src": "**/**.py", "use": "@vercel/python" }],
"probes": [{ "path": "/", "mustContain": "pip:RANDOMNESS_PLACEHOLDER" }]
}

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [{ "src": "**/**.py", "use": "@vercel/python" }],
"probes": [
{
"path": "/",
"mustContain": "pip:RANDOMNESS_PLACEHOLDER",
"logMustContain": "Warning: Invalid Python version \"3.7\" detected in Pipfile.lock will be ignored."
}
]
}

View File

@@ -1,4 +1,4 @@
import { join, dirname } from 'path';
import { join, dirname, relative } from 'path';
import execa from 'execa';
import {
ensureDir,
@@ -85,10 +85,12 @@ export async function build({
}: BuildOptions) {
await download(files, workPath, meta);
const entrypointFsDirname = join(workPath, dirname(entrypoint));
const gemfileName = 'Gemfile';
const gemfilePath = await walkParentDirs({
base: workPath,
start: entrypointFsDirname,
filename: 'Gemfile',
filename: gemfileName,
});
const gemfileContents = gemfilePath
? await readFile(gemfilePath, 'utf8')
@@ -130,15 +132,24 @@ export async function build({
'did not find a vendor directory but found a Gemfile, bundling gems...'
);
// 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;
const fileAtRoot = relative(workPath, gemfilePath) === gemfileName;
// If the `Gemfile` is located in the Root Directory of the project and
// the new File System API is used (`avoidTopLevelInstall`), the Install Command
// will have already installed its dependencies, so we don't need to do it again.
if (meta.avoidTopLevelInstall && fileAtRoot) {
debug('Skipping `bundle install` — already handled by Install Command');
} else {
// try installing. this won't work if native 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 {

View File

@@ -66,6 +66,23 @@ export async function installBundler(meta: Meta, gemfileContents: string) {
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...');
await execa(gemPath, ['install', 'bundler', '--no-document'], {
stdio: 'pipe',

View File

@@ -1,7 +1,7 @@
{
"name": "@vercel/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "1.2.8-canary.4",
"version": "1.2.8-canary.6",
"license": "MIT",
"main": "./dist/index",
"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"
ruby "~> 2.5.0"
ruby "~> 2.7.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,6 @@
{
"version": 2,
"builds": [{ "src": "index.rb", "use": "@vercel/ruby" }],
"build": { "env": { "RUBY_VERSION": "2.5.x" } },
"build": { "env": { "RUBY_VERSION": "2.7.x" } },
"probes": [{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" }]
}

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