Compare commits

..

37 Commits

Author SHA1 Message Date
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
Andy Bitz
f194d54b0c Publish Canary
- vercel@23.1.3-canary.37
2021-11-22 23:14:32 +01:00
Andy
6542086843 Revert "[cli] Switch from hardlinks to symlinks in vc build" (#7054)
* Revert "[cli] Switch from hardlinks to symlinks in vc build (#7026)"

This reverts commit 6dff0875f5.

* Keep copy logic
2021-11-22 23:12:46 +01:00
Andy Bitz
2721b3449d Publish Canary
- @vercel/build-utils@2.12.3-canary.20
 - vercel@23.1.3-canary.36
 - @vercel/client@10.2.3-canary.21
 - @vercel/node@1.12.2-canary.7
 - vercel-plugin-go@1.0.0-canary.5
 - vercel-plugin-node@1.12.2-canary.11
 - vercel-plugin-python@1.0.0-canary.5
 - vercel-plugin-ruby@1.0.0-canary.4
2021-11-22 13:22:56 +01:00
Andy
adb284519a [plugin-node][node] Fix dependency installation, handler path, and build script (#7043)
* [plugin-node][node] Fix dependency installation, handler path, and build script

* Use smaller library for testing

* Make test work on windows

* Remove .output before fixture runs and update routes update

* Apply suggestions from code review

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

Co-authored-by: Steven <steven@ceriously.com>
2021-11-22 13:21:20 +01:00
Steven
b2d91f3121 [cli] Fix metrics crash on some platforms (#7034) 2021-11-20 20:31:39 -05:00
Steven
32664cd13b Publish Canary
- @vercel/build-utils@2.12.3-canary.19
 - vercel@23.1.3-canary.35
 - @vercel/client@10.2.3-canary.20
 - @vercel/frameworks@0.5.1-canary.12
 - vercel-plugin-go@1.0.0-canary.4
 - vercel-plugin-node@1.12.2-canary.10
 - vercel-plugin-python@1.0.0-canary.4
 - vercel-plugin-ruby@1.0.0-canary.3
2021-11-20 10:20:05 -05:00
Sam Ko
db468c489a [frameworks] Fix Parcel default output directory to dist (#7029)
### Related Issues

The Parcel template via the Project Creating Flow returns a build error because the default output directory for the Parcel **Framework Preset** is searching for `public` instead of `dist`. The default behavior of Parcel's CLI I believe is `dist`.

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2021-11-20 01:44:22 +00:00
Steven
edd9bb506c [frameworks][cli] Consolidate buildCommand and devCommand (#7032)
* [frameworks] Consolidate buildCommand and devCommand

* Fix default build command when no override or package.json found
2021-11-19 19:07:52 -05:00
Steven
a72549a290 [tests] Make test more reliable for function timeout (#7033) 2021-11-19 17:26:32 -05:00
Steven
4aa6a13912 Publish Canary
- vercel@23.1.3-canary.34
 - vercel-plugin-python@1.0.0-canary.3
 - @vercel/python@2.0.6-canary.6
2021-11-19 14:42:02 -05:00
Steven
81ea0082f1 [python] Upgrade python 3.9 (#7008)
* [python] Upgrade python 3.9

* Fix python path

* Improve testing

* Add support for version selection via Pipfile

* Parse the "Pipfile.lock" instead

* Fix support for Node.js 12
2021-11-19 13:42:17 -05:00
Jared Palmer
6dff0875f5 [cli] Switch from hardlinks to symlinks in vc build (#7026)
* Switch from hardlinks to symlinks in vc build

* Remove error handling that fs-extra already takes care of

* Add comment

* Put back symlink error handling
2021-11-19 12:36:31 -05:00
Steven
30aa392c0a [tests] Increase CI duration (#7030)
Seems like macOS sometimes exceeds 60 min so this bumps the timeout.

The build can be between 3 minutes and 5 minutes. The tests can be between 45 minutes and 55 minutes.
2021-11-19 14:59:12 +00:00
Miroslav Simulcik
c4fc060030 [cli] Fix spinner output outside of TTY (#7017)
* Fix spinner output outside of TTY

* Use console.log

* Fix tests

* Revert "Fix tests"

This reverts commit 44383eea2dc3bb699d943079d77961ed86fe65d6.

* Revert "Use console.log"

This reverts commit 251bd1d624532e9ecfb4aee5b31538aa4ba740ef.
2021-11-17 14:04:59 +01:00
Steven
3fa08bf64f [cli] Fix update command to favor npm (#7018) 2021-11-16 16:07:31 -05:00
Steven
43056bde1f Publish Canary
- vercel@23.1.3-canary.33
2021-11-15 18:07:17 -05:00
Steven
a49966b9b4 [cli] Change vc build to use stdout (#7005) 2021-11-15 18:06:48 -05:00
Steven
7f55de71bb Publish Canary
- vercel@23.1.3-canary.32
 - @vercel/go@1.2.4-canary.4
 - vercel-plugin-go@1.0.0-canary.3
2021-11-15 13:37:04 -05:00
Steven
db8e36e04c [go] Add support for version 1.17 (#7002)
* Support more Go versions

Co-authored-by: Stefan M <StefMa@users.noreply.github.com>
2021-11-15 13:36:26 -05:00
Andy Bitz
82924bb5c4 Publish Canary
- vercel@23.1.3-canary.31
 - vercel-plugin-node@1.12.2-canary.9
2021-11-15 14:11:55 +01:00
Andy
18b5fac93e [vercel-plugin-node] Use .nft.json files instead of directories. (#6988)
* Ignore install step if package.json does not exist in entrypoint directory

* [plugin-node] Use `.nft.json` files

* Bump @vercel/fun

* Update package.json

* Remove package

* Remove import

* Update paths

* Revert "Update paths"

This reverts commit fa304e90381f475c64b2333402dca489813522da.

* Update path for windows

* Update handler path
2021-11-15 14:08:49 +01:00
Miroslav Simulcik
a6012e600b [cli] Fix updating of gitignore in vercel link (#6986)
* Fix updating of gitignore in link

* Fix modification of gitignore file during linking

* Fix empty file case
2021-11-12 17:11:21 +01:00
Steven
c3abf73f58 Publish Canary
- @vercel/build-utils@2.12.3-canary.18
 - vercel@23.1.3-canary.30
 - @vercel/client@10.2.3-canary.19
 - vercel-plugin-go@1.0.0-canary.2
 - vercel-plugin-node@1.12.2-canary.8
 - vercel-plugin-python@1.0.0-canary.2
 - vercel-plugin-ruby@1.0.0-canary.2
2021-11-11 14:54:33 -05:00
Steven
4873b8b379 [build-utils][node] Add updateFunctionsManifest function (#6971) 2021-11-11 14:53:43 -05:00
Miroslav Simulcik
6248139281 [cli] Prevent recursion of vercel build and vercel dev (#6960)
* Prevent infinite recursion of vercel build and dev

* Improved text

* Improved the text further

* Add tests

* Debug test

* Add debug log

* Debug test

* debug test

* Debug test

* Fix test

* Debug tests

* Debug test

* Remove only from tests

* Add error documentation

* Improved docs

* Fix other test

* Fixed links to error docs

* Remove legacy error page

* Use nicer error links

Co-authored-by: Leo Lamprecht <leo@vercel.com>
2021-11-11 10:51:30 -05:00
Miroslav Simulcik
507a5de3cd [tests] Fix getting of config paths (#6969)
Co-authored-by: Steven <steven@ceriously.com>
2021-11-09 14:34:41 -05:00
Gary Borton
be1c78e72f Publish Canary
- vercel@23.1.3-canary.29
 - vercel-plugin-middleware@0.0.0-canary.7
2021-11-09 11:07:01 -08:00
Gary Borton
c277c649c6 Fix middleware build result. Return middleware next when invalid result. (#6963)
* Fix middleware build result. Return middleware next when invalid result.

* Add a unit test that would have failed before previous commit.

* Copy next.js behavior.

* Add a test for invalid responses from middleware.

Co-authored-by: Leo Lamprecht <leo@vercel.com>
2021-11-09 11:06:16 -08:00
95 changed files with 2553 additions and 668 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

@@ -11,7 +11,7 @@ on:
jobs:
test:
name: Dev
timeout-minutes: 60
timeout-minutes: 75
strategy:
fail-fast: false
matrix:

1
.gitignore vendored
View File

@@ -27,3 +27,4 @@ test/lib/deployment/failed-page.txt
/public
__pycache__
.vercel
.output

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

@@ -14,8 +14,6 @@ const frameworks = (_frameworks as Framework[])
sort: undefined,
dependency: undefined,
defaultRoutes: undefined,
devCommand: undefined,
buildCommand: undefined,
};
if (framework.logo) {

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

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

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

@@ -0,0 +1,57 @@
# Welcome to Remix!
- [Remix Docs](https://remix.run/docs)
## Vercel Setup
First you'll need the [Vercel CLI](https://vercel.com/docs/cli):
```sh
npm i -g vercel
```
Before you can run the app in development, you need to link this project to a new Vercel project on your account.
**It is important that you use a new project. If you try to link this project to an existing project (like a Next.js site) you will have problems.**
```sh
$ vercel link
```
Follow the prompts, and when it's done you should be able to get started.
## Development
You will be running two processes during development when using Vercel as your server.
- Your Vercel server in one
- The Remix development server in another
```sh
# in one tab
$ vercel dev
# in another
$ npm run dev
```
Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go!
If you'd rather run everything in a single tab, you can look at [concurrently](https://npm.im/concurrently) or similar tools to run both processes in one tab.
## Deploying
```sh
$ npm run build
# preview deployment
$ vercel
# production deployment
$ vercel --prod
```
### GitHub Automatic Deployments
For some reason the GitHub integration doesn't deploy the public folder. We're working with Vercel to figure this out.
For now, [you can set up a GitHub action with this config](https://gist.github.com/mcansh/91f8effda798b41bb373351fad217070) from our friend [@mcansh](https://github.com/mcansh).

View File

@@ -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
});
}

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

@@ -0,0 +1,247 @@
import * as React from "react";
import {
Link,
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useCatch,
useLocation
} from "remix";
import type { LinksFunction } from "remix";
import deleteMeRemixStyles from "~/styles/demos/remix.css";
import globalStylesUrl from "~/styles/global.css";
import darkStylesUrl from "~/styles/dark.css";
/**
* The `links` export is a function that returns an array of objects that map to
* the attributes for an HTML `<link>` element. These will load `<link>` tags on
* every route in the app, but individual routes can include their own links
* that are automatically unloaded when a user navigates away from the route.
*
* https://remix.run/api/app#links
*/
export let links: LinksFunction = () => {
return [
{ rel: "stylesheet", href: globalStylesUrl },
{
rel: "stylesheet",
href: darkStylesUrl,
media: "(prefers-color-scheme: dark)"
},
{ rel: "stylesheet", href: deleteMeRemixStyles }
];
};
/**
* The root module's default export is a component that renders the current
* route via the `<Outlet />` component. Think of this as the global layout
* component for your app.
*/
export default function App() {
return (
<Document>
<Layout>
<Outlet />
</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}
<RouteChangeAnnouncement />
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === "development" && <LiveReload />}
</body>
</html>
);
}
function Layout({ children }: React.PropsWithChildren<{}>) {
return (
<div className="remix-app">
<header className="remix-app__header">
<div className="container remix-app__header-content">
<Link to="/" title="Remix" className="remix-app__header-home-link">
<RemixLogo />
</Link>
<nav aria-label="Main navigation" className="remix-app__header-nav">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<a href="https://remix.run/docs">Remix Docs</a>
</li>
<li>
<a href="https://github.com/remix-run/remix">GitHub</a>
</li>
</ul>
</nav>
</div>
</header>
<div className="remix-app__main">
<div className="container remix-app__main-content">{children}</div>
</div>
<footer className="remix-app__footer">
<div className="container remix-app__footer-content">
<p>&copy; You!</p>
</div>
</footer>
</div>
);
}
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>
);
}
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>
);
}
function RemixLogo(props: React.ComponentPropsWithoutRef<"svg">) {
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"
{...props}
>
<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>
);
}
/**
* Provides an alert for screen reader users when the route changes.
*/
const RouteChangeAnnouncement = React.memo(() => {
let [hydrated, setHydrated] = React.useState(false);
let [innerHtml, setInnerHtml] = React.useState("");
let location = useLocation();
React.useEffect(() => {
setHydrated(true);
}, []);
let firstRenderRef = React.useRef(true);
React.useEffect(() => {
// Skip the first render because we don't want an announcement on the
// initial page load.
if (firstRenderRef.current) {
firstRenderRef.current = false;
return;
}
let pageTitle = location.pathname === "/" ? "Home page" : document.title;
setInnerHtml(`Navigated to ${pageTitle}`);
}, [location.pathname]);
// Render nothing on the server. The live region provides no value unless
// scripts are loaded and the browser takes over normal routing.
if (!hydrated) {
return null;
}
return (
<div
aria-live="assertive"
aria-atomic
id="route-change-region"
style={{
border: "0",
clipPath: "inset(100%)",
clip: "rect(0 0 0 0)",
height: "1px",
margin: "-1px",
overflow: "hidden",
padding: "0",
position: "absolute",
width: "1px",
whiteSpace: "nowrap",
wordWrap: "normal"
}}
>
{innerHtml}
</div>
);
});

View File

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

View File

@@ -0,0 +1,98 @@
/*
* 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;
}

View File

@@ -0,0 +1,28 @@
{
"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.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"remix": "^1.0.4",
"@remix-run/vercel": "^1.0.4"
},
"devDependencies": {
"@remix-run/dev": "^1.0.4",
"@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.17",
"version": "2.12.3-canary.21",
"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.11",
"@vercel/frameworks": "0.5.1-canary.13",
"@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",

View File

@@ -2,7 +2,7 @@ import fs from 'fs-extra';
import { join, dirname, relative } from 'path';
import glob from './fs/glob';
import { normalizePath } from './fs/normalize-path';
import { FILES_SYMBOL, Lambda } from './lambda';
import { FILES_SYMBOL, getLambdaOptionsFromFunction, Lambda } from './lambda';
import type FileBlob from './file-blob';
import type { BuilderFunctions, BuildOptions, Files } from './types';
import minimatch from 'minimatch';
@@ -16,13 +16,20 @@ export function convertRuntimeToPlugin(
buildRuntime: (options: BuildOptions) => Promise<{ output: Lambda }>,
ext: string
) {
return async function build({ workPath }: { workPath: string }) {
// This `build()` signature should match `plugin.build()` signature in `vercel build`.
return async function build({
vercelConfig,
workPath,
}: {
vercelConfig: { functions?: BuilderFunctions; regions?: string[] };
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);
const functionsManifest: { [key: string]: any } = {};
const functions = await readVercelConfigFunctions(workPath);
const pages: { [key: string]: any } = {};
const { functions = {} } = vercelConfig;
const traceDir = join(workPath, '.output', 'runtime-traced-files');
await fs.ensureDir(traceDir);
@@ -44,11 +51,11 @@ export function convertRuntimeToPlugin(
},
});
functionsManifest[entrypoint] = {
pages[entrypoint] = {
handler: output.handler,
runtime: output.runtime,
memory: config.memory || output.memory,
maxDuration: config.maxDuration || output.maxDuration,
memory: output.memory,
maxDuration: output.maxDuration,
environment: output.environment,
allowQuery: output.allowQuery,
regions: output.regions,
@@ -98,10 +105,7 @@ export function convertRuntimeToPlugin(
await fs.writeFile(nft, json);
}
await fs.writeFile(
join(workPath, '.output', 'functions-manifest.json'),
JSON.stringify(functionsManifest)
);
await updateFunctionsManifest({ vercelConfig, workPath, pages });
};
}
@@ -115,14 +119,10 @@ async function linkOrCopy(existingPath: string, newPath: string) {
}
}
async function readVercelConfigFunctions(
workPath: string
): Promise<BuilderFunctions> {
const vercelJsonPath = join(workPath, 'vercel.json');
async function readJson(filePath: string): Promise<{ [key: string]: any }> {
try {
const str = await fs.readFile(vercelJsonPath, 'utf8');
const obj = JSON.parse(str);
return obj.functions || {};
const str = await fs.readFile(filePath, 'utf8');
return JSON.parse(str);
} catch (err) {
if (err.code === 'ENOENT') {
return {};
@@ -130,3 +130,74 @@ async function readVercelConfigFunctions(
throw err;
}
}
/**
* 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.
*/
export async function updateFunctionsManifest({
vercelConfig,
workPath,
pages,
}: {
vercelConfig: { functions?: BuilderFunctions; regions?: string[] };
workPath: string;
pages: { [key: string]: any };
}) {
const functionsManifestPath = join(
workPath,
'.output',
'functions-manifest.json'
);
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,
};
}
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.
*/
export async function updateRoutesManifest({
workPath,
dynamicRoutes,
}: {
workPath: string;
dynamicRoutes?: {
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.pages404 === undefined) routesManifest.pages404 = true;
if (dynamicRoutes) {
if (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = [];
routesManifest.dynamicRoutes.push(...dynamicRoutes);
}
await fs.writeFile(routesManifestPath, JSON.stringify(routesManifest));
}

View File

@@ -82,7 +82,11 @@ export { detectFramework } from './detect-framework';
export { DetectorFilesystem } from './detectors/filesystem';
export { readConfigFile } from './fs/read-config-file';
export { normalizePath } from './fs/normalize-path';
export { convertRuntimeToPlugin } from './convert-runtime-to-plugin';
export {
convertRuntimeToPlugin,
updateFunctionsManifest,
updateRoutesManifest,
} from './convert-runtime-to-plugin';
export * from './schemas';
export * from './types';

View File

@@ -36,7 +36,7 @@ interface CreateLambdaOptions {
interface GetLambdaOptionsFromFunctionOptions {
sourceFile: string;
config?: Config;
config?: Pick<Config, 'functions'>;
}
export const FILES_SYMBOL = Symbol('files');

View File

@@ -44,10 +44,11 @@ describe('convert-runtime-to-plugin', () => {
};
const lambdaFiles = await fsToJson(workPath);
const vercelConfig = JSON.parse(lambdaFiles['vercel.json']);
delete lambdaFiles['vercel.json'];
const build = await convertRuntimeToPlugin(buildRuntime, '.py');
await build({ workPath });
await build({ vercelConfig, workPath });
const output = await fsToJson(join(workPath, '.output'));
expect(output).toMatchObject({
@@ -71,9 +72,12 @@ describe('convert-runtime-to-plugin', () => {
const funcManifest = JSON.parse(output['functions-manifest.json']);
expect(funcManifest).toMatchObject({
'api/index.py': lambdaOptions,
'api/users/get.py': lambdaOptions,
'api/users/post.py': { ...lambdaOptions, memory: 3008 },
version: 1,
pages: {
'api/index.py': lambdaOptions,
'api/users/get.py': lambdaOptions,
'api/users/post.py': { ...lambdaOptions, memory: 3008 },
},
});
const indexJson = JSON.parse(output.server.pages.api['index.py.nft.json']);

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "23.1.3-canary.28",
"version": "23.1.3-canary.39",
"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.17",
"@vercel/go": "1.2.4-canary.3",
"@vercel/node": "1.12.2-canary.6",
"@vercel/python": "2.0.6-canary.5",
"@vercel/build-utils": "2.12.3-canary.21",
"@vercel/go": "1.2.4-canary.4",
"@vercel/node": "1.12.2-canary.7",
"@vercel/python": "2.1.2-canary.0",
"@vercel/ruby": "1.2.8-canary.4",
"update-notifier": "4.1.0",
"vercel-plugin-middleware": "0.0.0-canary.6",
"vercel-plugin-node": "1.12.2-canary.7"
"vercel-plugin-middleware": "0.0.0-canary.7",
"vercel-plugin-node": "1.12.2-canary.12"
},
"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.11",
"@vercel/frameworks": "0.5.1-canary.13",
"@vercel/ncc": "0.24.0",
"@vercel/nft": "0.17.0",
"@zeit/fun": "0.11.2",

View File

@@ -17,6 +17,7 @@ import ogGlob from 'glob';
import { 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';
@@ -30,10 +31,7 @@ import { getCommandName, getPkgName } from '../util/pkg-name';
import { loadCliPlugins } from '../util/plugins';
import { findFramework } from '../util/projects/find-framework';
import { VERCEL_DIR } from '../util/projects/link';
import {
ProjectLinkAndSettings,
readProjectSettings,
} from '../util/projects/project-settings';
import { readProjectSettings } from '../util/projects/project-settings';
import pull from './pull';
const sema = new Sema(16, {
@@ -68,16 +66,23 @@ const help = () => {
const OUTPUT_DIR = '.output';
const fields: {
name: string;
value: keyof ProjectLinkAndSettings['settings'];
}[] = [
{ name: 'Build Command', value: 'buildCommand' },
{ name: 'Output Directory', value: 'outputDirectory' },
{ name: 'Root Directory', value: 'rootDirectory' },
];
export default async function main(client: Client) {
if (process.env.__VERCEL_BUILD_RUNNING) {
client.output.error(
`${cmd(
`${getPkgName()} build`
)} must not recursively invoke itself. Check the Build Command in the Project Settings or the ${cmd(
'build'
)} script in ${cmd('package.json')}`
);
client.output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
);
return 1;
} else {
process.env.__VERCEL_BUILD_RUNNING = '1';
}
let argv;
const buildStamp = stamp();
try {
@@ -152,47 +157,57 @@ export default async function main(client: Client) {
}
const buildState = { ...project.settings };
client.output.log(`Retrieved Project Settings:`);
client.output.print(
chalk.dim(` - ${chalk.bold(`Framework Preset:`)} ${framework.name}\n`)
const formatSetting = (
name: string,
override: string | null | undefined,
defaults: typeof framework.settings.outputDirectory
) =>
` - ${chalk.bold(`${name}:`)} ${`${
override
? override + ` (override)`
: 'placeholder' in defaults
? chalk.italic(`${defaults.placeholder}`)
: defaults.value
}`}`;
console.log(`Retrieved Project Settings:`);
console.log(
chalk.dim(` - ${chalk.bold(`Framework Preset:`)} ${framework.name}`)
);
console.log(
chalk.dim(
formatSetting(
'Build Command',
project.settings.buildCommand,
framework.settings.buildCommand
)
)
);
console.log(
chalk.dim(
formatSetting(
'Output Directory',
project.settings.outputDirectory,
framework.settings.outputDirectory
)
)
);
for (let field of fields) {
const defaults = (framework.settings as any)[field.value];
if (defaults) {
client.output.print(
chalk.dim(
` - ${chalk.bold(`${field.name}:`)} ${`${
project.settings[field.value]
? project.settings[field.value] + ` (override)`
: isSettingValue(defaults)
? defaults.value
: chalk.italic(`${defaults.placeholder}`)
}`}\n`
)
);
}
if (field.value != 'buildCommand') {
(buildState as any)[field.value] = project.settings[field.value]
? project.settings[field.value]
: defaults
? isSettingValue(defaults)
? defaults.value
: null
: null;
}
}
buildState.outputDirectory =
project.settings.outputDirectory ||
(isSettingValue(framework.settings.outputDirectory)
? framework.settings.outputDirectory.value
: null);
buildState.rootDirectory = project.settings.rootDirectory;
if (loadedEnvFiles.length > 0) {
client.output.log(
console.log(
`Loaded Environment Variables from ${loadedEnvFiles.length} ${pluralize(
'file',
loadedEnvFiles.length
)}:`
);
for (let envFile of loadedEnvFiles) {
client.output.print(chalk.dim(` - ${envFile.path}\n`));
console.log(chalk.dim(` - ${envFile.path}`));
}
}
@@ -223,7 +238,7 @@ export default async function main(client: Client) {
};
if (plugins?.pluginCount && plugins?.pluginCount > 0) {
client.output.log(
console.log(
`Loaded ${plugins.pluginCount} CLI ${pluralize(
'Plugin',
plugins.pluginCount
@@ -231,7 +246,7 @@ export default async function main(client: Client) {
);
// preBuild Plugins
if (plugins.preBuildPlugins.length > 0) {
client.output.log(
console.log(
`Running ${plugins.pluginCount} CLI ${pluralize(
'Plugin',
plugins.pluginCount
@@ -269,17 +284,19 @@ export default async function main(client: Client) {
// Clean the output directory
fs.removeSync(join(cwd, OUTPUT_DIR));
// Yarn v2 PnP mode may be activated, so force
// "node-modules" linker style
const env = {
YARN_NODE_LINKER: 'node-modules',
...spawnOpts.env,
};
if (typeof buildState.buildCommand === 'string') {
client.output.log(`Running Build Command: ${cmd(buildState.buildCommand)}`);
console.log(`Running Build Command: ${cmd(buildState.buildCommand)}`);
await execCommand(buildState.buildCommand, {
...spawnOpts,
// Yarn v2 PnP mode may be activated, so force
// "node-modules" linker style
env: {
YARN_NODE_LINKER: 'node-modules',
...spawnOpts.env,
},
cwd: cwd,
env,
cwd,
});
} else if (fs.existsSync(join(cwd, 'package.json'))) {
await runPackageJsonScript(
@@ -288,12 +305,23 @@ export default async function main(client: Client) {
['vercel-build', 'now-build', 'build'],
spawnOpts
);
} else if (typeof framework.settings.buildCommand.value === 'string') {
console.log(
`Running Build Command: ${cmd(framework.settings.buildCommand.value)}`
);
await execCommand(framework.settings.buildCommand.value, {
...spawnOpts,
env,
cwd,
});
}
// don't trust framework detection here because they might be switching to next on a branch
const isNextJs = fs.existsSync(join(cwd, '.next'));
if (!fs.existsSync(join(cwd, OUTPUT_DIR))) {
let outputDir = join(OUTPUT_DIR, 'static');
let distDir = await framework.getFsOutputDir(cwd);
if (framework.slug === 'nextjs') {
if (isNextJs) {
outputDir = OUTPUT_DIR;
}
const copyStamp = stamp();
@@ -336,7 +364,7 @@ export default async function main(client: Client) {
)
);
client.output.stopSpinner();
client.output.log(
console.log(
`Copied ${files.length.toLocaleString()} files from ${param(
distDir
)} to ${param(outputDir)} ${copyStamp()}`
@@ -377,7 +405,7 @@ export default async function main(client: Client) {
}
// Special Next.js processing.
if (framework.slug === 'nextjs') {
if (isNextJs) {
// The contents of `.output/static` should be placed inside of `.output/static/_next/static`
const tempStatic = '___static';
await fs.rename(
@@ -557,12 +585,20 @@ export default async function main(client: Client) {
// Build Plugins
if (plugins?.buildPlugins && plugins.buildPlugins.length > 0) {
client.output.log(
console.log(
`Running ${plugins.pluginCount} CLI ${pluralize(
'Plugin',
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') {
@@ -575,6 +611,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(
@@ -592,13 +629,13 @@ export default async function main(client: Client) {
}
}
client.output.print(
console.log(
`${prependEmoji(
`Build Completed in ${chalk.bold(OUTPUT_DIR)} ${chalk.gray(
buildStamp()
)}`,
emoji('success')
)}\n`
)}`
);
return 0;
@@ -644,25 +681,37 @@ export async function runPackageJsonScript(
}
}
client.output.log(`Running Build Command: ${cmd(opts.prettyCommand)}\n`);
console.log(`Running Build Command: ${cmd(opts.prettyCommand)}\n`);
await spawnAsync(cliType, ['run', scriptName], opts);
client.output.print('\n'); // give it some room
console.log(); // give it some room
client.output.debug(`Script complete [${Date.now() - runScriptTime}ms]`);
return true;
}
async function linkOrCopy(existingPath: string, newPath: string) {
try {
await fs.createLink(existingPath, newPath);
if (
newPath.endsWith('.nft.json') ||
newPath.endsWith('middleware-manifest.json') ||
newPath.endsWith('required-server-files.json')
) {
await fs.copy(existingPath, newPath, {
overwrite: true,
});
} else {
await fs.createLink(existingPath, newPath);
}
} catch (err: any) {
// eslint-disable-line
// If a hard link to the same file already exists
// If a symlink to the same file already exists
// then trying to copy it will make an empty file from it.
if (err['code'] === 'EEXIST') return;
// In some VERY rare cases (1 in a thousand), hard-link creation fails on Windows.
// In some VERY rare cases (1 in a thousand), symlink creation fails on Windows.
// In that case, we just fall back to copying.
// This issue is reproducible with "pnpm add @material-ui/icons@4.9.1"
await fs.copyFile(existingPath, newPath);
await fs.copy(existingPath, newPath, {
overwrite: true,
});
}
}

View File

@@ -6,7 +6,6 @@ import { ProjectEnvVariable } from '../../types';
import Client from '../../util/client';
import { getLinkedProject } from '../../util/projects/link';
import { getFrameworks } from '../../util/get-frameworks';
import { isSettingValue } from '../../util/is-setting-value';
import { ProjectSettings } from '../../types';
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
import setupAndLink from '../../util/link/setup-and-link';
@@ -71,9 +70,9 @@ export default async function dev(
frameworkSlug = framework.slug;
}
const defaults = framework.settings.devCommand;
if (isSettingValue(defaults)) {
devCommand = defaults.value;
const defaults = framework.settings.devCommand.value;
if (defaults) {
devCommand = defaults;
}
}
}

View File

@@ -48,6 +48,22 @@ const help = () => {
};
export default async function main(client: Client) {
if (process.env.__VERCEL_DEV_RUNNING) {
client.output.error(
`${cmd(
`${getPkgName()} dev`
)} must not recursively invoke itself. Check the Development Command in the Project Settings or the ${cmd(
'dev'
)} script in ${cmd('package.json')}`
);
client.output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
);
return 1;
} else {
process.env.__VERCEL_DEV_RUNNING = '1';
}
let argv;
let args;
const { output } = client;
@@ -90,22 +106,21 @@ export default async function main(client: Client) {
if (pkg) {
const { scripts } = pkg as PackageJson;
if (scripts && scripts.dev && /\bnow\b\W+\bdev\b/.test(scripts.dev)) {
output.error(
`The ${cmd('dev')} script in ${cmd(
'package.json'
)} must not contain ${cmd('now dev')}`
if (
scripts &&
scripts.dev &&
/\b(now|vercel)\b\W+\bdev\b/.test(scripts.dev)
) {
client.output.error(
`${cmd(
`${getPkgName()} dev`
)} must not recursively invoke itself. Check the Development Command in the Project Settings or the ${cmd(
'dev'
)} script in ${cmd('package.json')}`
);
output.error(`Learn More: http://err.sh/vercel/now-dev-as-dev-script`);
return 1;
}
if (scripts && scripts.dev && /\bvercel\b\W+\bdev\b/.test(scripts.dev)) {
output.error(
`The ${cmd('dev')} script in ${cmd(
'package.json'
)} must not contain ${cmd('vercel dev')}`
client.output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
);
output.error(`Learn More: http://err.sh/vercel/now-dev-as-dev-script`);
return 1;
}
}

View File

@@ -160,24 +160,26 @@ const main = async () => {
// * a path to deploy (as in: `vercel path/`)
// * a subcommand (as in: `vercel ls`)
const targetOrSubcommand = argv._[2];
const isBuildOrDev =
targetOrSubcommand === 'build' || targetOrSubcommand === 'dev';
output.print(
`${chalk.grey(
`${getTitleName()} CLI ${pkg.version}${
targetOrSubcommand === 'dev'
? ' dev (beta)'
: targetOrSubcommand === 'build'
? ' build (beta)'
: ''
}${
isCanary ||
targetOrSubcommand === 'dev' ||
targetOrSubcommand === 'build'
? ' — https://vercel.com/feedback'
: ''
}`
)}\n`
);
if (isBuildOrDev) {
console.log(
`${chalk.grey(
`${getTitleName()} CLI ${
pkg.version
} ${targetOrSubcommand} (beta) — https://vercel.com/feedback`
)}`
);
} else {
output.print(
`${chalk.grey(
`${getTitleName()} CLI ${pkg.version}${
isCanary ? ' — https://vercel.com/feedback' : ''
}`
)}\n`
);
}
// Handle `--version` directly
if (!targetOrSubcommand && argv['--version']) {

View File

@@ -18,12 +18,8 @@ export const isDirectory = (path: string): boolean => {
const getGlobalPathConfig = (): string => {
let customPath: string | undefined;
try {
const argv = getArgs(process.argv.slice(2), {});
customPath = argv['--global-config'];
} catch (_error) {
// args are optional so consume error
}
const argv = getArgs(process.argv.slice(2), {}, { permissive: true });
customPath = argv['--global-config'];
const vercelDirectories = XDGAppPaths('com.vercel.cli').dataDirs();

View File

@@ -7,12 +7,8 @@ import getArgs from '../../util/get-args';
export default function getLocalPathConfig(prefix: string) {
let customPath: string | undefined;
try {
const argv = getArgs(process.argv.slice(2), {});
customPath = argv['--local-config'];
} catch (_error) {
// args are optional so consume error
}
const argv = getArgs(process.argv.slice(2), {}, { permissive: true });
customPath = argv['--local-config'];
// If `--local-config` flag was specified, then that takes priority
if (customPath) {

View File

@@ -1358,13 +1358,20 @@ export default class DevServer {
this.cwd,
this.output
);
for (let plugin of devMiddlewarePlugins) {
const result = await plugin.plugin.runDevMiddleware(req, res, this.cwd);
if (result.finished) {
return result;
try {
for (let plugin of devMiddlewarePlugins) {
const result = await plugin.plugin.runDevMiddleware(req, res, this.cwd);
if (result.finished) {
return result;
}
}
return { finished: false };
} catch (e) {
return {
finished: true,
error: e,
};
}
return { finished: false };
};
/**
@@ -2154,7 +2161,10 @@ export default class DevServer {
process.stdout.write(data.replace(proxyPort, devPort));
});
p.on('exit', () => {
p.on('exit', (code: number) => {
if (code > 0) {
process.exit(code);
}
this.devProcessPort = undefined;
});

View File

@@ -1,11 +1,9 @@
import { Stats } from 'fs';
import { sep, dirname, join, resolve } from 'path';
import { readJSON, lstat, readlink, readFile, realpath } from 'fs-extra';
import { lstat, readlink, readFile, realpath } from 'fs-extra';
import { isCanary } from './is-canary';
import { getPkgName } from './pkg-name';
// `npm` tacks a bunch of extra properties on the `package.json` file,
// so check for one of them to determine yarn vs. npm.
async function isYarn(): Promise<boolean> {
let s: Stats;
let binPath = process.argv[1];
@@ -20,8 +18,12 @@ async function isYarn(): Promise<boolean> {
}
}
const pkgPath = join(dirname(binPath), '..', 'package.json');
const pkg = await readJSON(pkgPath).catch(() => ({}));
return !('_id' in pkg);
/*
* Generally, pkgPath looks like:
* "/Users/username/.config/yarn/global/node_modules/vercel/package.json"
* "/usr/local/share/.config/yarn/global/node_modules/vercel/package.json"
*/
return pkgPath.includes(join('yarn', 'global'));
}
async function getConfigPrefix() {

View File

@@ -1,6 +1,5 @@
import crypto from 'crypto';
import ua from 'universal-analytics';
import { platform, release, userInfo } from 'os';
import { getPlatformEnv } from '@vercel/build-utils';
import userAgent from './ua-browser';
@@ -16,10 +15,15 @@ export const shouldCollectMetrics =
export const metrics = (): ua.Visitor => {
const token =
typeof config.token === 'string' ? config.token : platform() + release();
const salt = userInfo().username;
typeof config.token === 'string'
? config.token
: process.platform + process.arch;
const salt =
(process.env.USER || '') +
(process.env.LANG || '') +
(process.env.SHELL || '');
const hash = crypto
.pbkdf2Sync(token, salt, 1000, 64, 'sha512')
.pbkdf2Sync(token, salt, 100, 64, 'sha512')
.toString('hex')
.substring(0, 24);

View File

@@ -125,10 +125,14 @@ export class Output {
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
return;
}
if (this._spinner) {
this._spinner.text = message;
if (this.isTTY) {
if (this._spinner) {
this._spinner.text = message;
} else {
this._spinner = wait(message, delay);
}
} else {
this._spinner = wait(message, delay);
this.print(`${message}\n`);
}
};

View File

@@ -244,20 +244,27 @@ export async function linkFolderToProject(
try {
const gitIgnorePath = join(path, '.gitignore');
const gitIgnore = await readFile(gitIgnorePath, 'utf8').catch(() => null);
const EOL = gitIgnore && gitIgnore.includes('\r\n') ? '\r\n' : os.EOL;
let gitIgnore =
(await readFile(gitIgnorePath, 'utf8').catch(() => null)) ?? '';
const EOL = gitIgnore.includes('\r\n') ? '\r\n' : os.EOL;
let contentModified = false;
if (
!gitIgnore ||
!gitIgnore.split(EOL).includes(VERCEL_DIR) ||
!gitIgnore.split(EOL).includes(VERCEL_OUTPUT_DIR)
) {
await writeFile(
gitIgnorePath,
gitIgnore
? `${gitIgnore}${EOL}${VERCEL_DIR}${EOL}${VERCEL_OUTPUT_DIR}${EOL}`
: `${VERCEL_DIR}${EOL}${VERCEL_OUTPUT_DIR}${EOL}`
);
if (!gitIgnore.split(EOL).includes(VERCEL_DIR)) {
gitIgnore += `${
gitIgnore.endsWith(EOL) || gitIgnore.length === 0 ? '' : EOL
}${VERCEL_DIR}${EOL}`;
contentModified = true;
}
if (!gitIgnore.split(EOL).includes(VERCEL_OUTPUT_DIR)) {
gitIgnore += `${
gitIgnore.endsWith(EOL) || gitIgnore.length === 0 ? '' : EOL
}${VERCEL_OUTPUT_DIR}${EOL}`;
contentModified = true;
}
if (contentModified) {
await writeFile(gitIgnorePath, gitIgnore);
isGitIgnoreUpdated = true;
}
} catch (error) {

View File

@@ -0,0 +1,3 @@
export default function () {
return 'freecandy';
}

View File

@@ -19,7 +19,7 @@ const getRevertAliasConfigFile = () => {
],
});
};
module.exports = async function prepare(session) {
module.exports = async function prepare(session, binaryPath) {
const spec = {
'static-single-file': {
'first.png': getImageFile(session, { size: 30 }),
@@ -114,6 +114,23 @@ module.exports = async function prepare(session) {
2
),
},
'dev-fail-on-recursion-command': {
'package.json': '{}',
},
'build-fail-on-recursion-command': {
'package.json': '{}',
},
'build-fail-on-recursion-script': {
'package.json': JSON.stringify(
{
scripts: {
build: `${binaryPath} build`,
},
},
null,
2
),
},
'static-deployment': {
'index.txt': 'Hello World',
},
@@ -348,6 +365,10 @@ module.exports = async function prepare(session) {
'project-link-dev': {
'package.json': '{}',
},
'project-link-gitignore': {
'package.json': '{}',
'.gitignore': '.output',
},
'project-link-legacy': {
'index.html': 'Hello',
'vercel.json': '{"builds":[{"src":"*.html","use":"@vercel/static"}]}',

View File

@@ -252,10 +252,69 @@ const createUser = async () => {
const getConfigAuthPath = () => path.join(globalDir, 'auth.json');
async function setupProject(process, projectName, overrides) {
await waitForPrompt(process, chunk => /Set up [^?]+\?/.test(chunk));
process.stdin.write('yes\n');
await waitForPrompt(process, chunk => /Which scope [^?]+\?/.test(chunk));
process.stdin.write('\n');
await waitForPrompt(process, chunk =>
chunk.includes('Link to existing project?')
);
process.stdin.write('no\n');
await waitForPrompt(process, chunk =>
chunk.includes('Whats your projects name?')
);
process.stdin.write(`${projectName}\n`);
await waitForPrompt(process, chunk =>
chunk.includes('In which directory is your code located?')
);
process.stdin.write('\n');
await waitForPrompt(process, chunk =>
chunk.includes('Want to override the settings?')
);
if (overrides) {
process.stdin.write('yes\n');
const { buildCommand, outputDirectory, devCommand } = overrides;
await waitForPrompt(process, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
process.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Build Command?`)
);
process.stdin.write(`${buildCommand ?? ''}\n`);
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Output Directory?`)
);
process.stdin.write(`${outputDirectory ?? ''}\n`);
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Development Command?`)
);
process.stdin.write(`${devCommand ?? ''}\n`);
} else {
process.stdin.write('no\n');
}
await waitForPrompt(process, chunk => chunk.includes('Linked to'));
}
test.before(async () => {
try {
await createUser();
await prepareFixtures(contextName);
await prepareFixtures(contextName, binaryPath);
} catch (err) {
console.log('Failed `test.before`');
console.log(err);
@@ -2227,13 +2286,93 @@ test('whoami', async t => {
t.is(stdout, contextName, formatOutput({ stdout, stderr }));
});
test('fail `now dev` dev script without now.json', async t => {
test('[vercel dev] fails when dev script calls vercel dev recursively', async t => {
const deploymentPath = fixture('now-dev-fail-dev-script');
const { exitCode, stderr } = await execute(['dev', deploymentPath]);
t.is(exitCode, 1);
t.true(
stderr.includes('must not contain `now dev`'),
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel dev] fails when development commad calls vercel dev recursively', async t => {
const dir = fixture('dev-fail-on-recursion-command');
const projectName = `dev-fail-on-recursion-command-${
Math.random().toString(36).split('.')[1]
}`;
const dev = execa(binaryPath, ['dev', ...defaultArgs], {
cwd: dir,
reject: false,
});
await setupProject(dev, projectName, {
devCommand: `${binaryPath} dev`,
});
const { exitCode, stderr } = await dev;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel build] fails when build commad calls vercel build recursively', async t => {
const dir = fixture('build-fail-on-recursion-command');
const projectName = `build-fail-on-recursion-command-${
Math.random().toString(36).split('.')[1]
}`;
const build = execa(binaryPath, ['build', ...defaultArgs], {
cwd: dir,
reject: false,
});
await waitForPrompt(build, chunk =>
chunk.includes('No Project Settings found locally')
);
build.stdin.write('yes\n');
await setupProject(build, projectName, {
buildCommand: `${binaryPath} build`,
});
const { exitCode, stderr } = await build;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel build] fails when build script calls vercel build recursively', async t => {
const dir = fixture('build-fail-on-recursion-script');
const projectName = `build-fail-on-recursion-script-${
Math.random().toString(36).split('.')[1]
}`;
const build = execa(binaryPath, ['build', ...defaultArgs], {
cwd: dir,
reject: false,
});
await waitForPrompt(build, chunk =>
chunk.includes('No Project Settings found locally')
);
build.stdin.write('yes\n');
await setupProject(build, projectName);
const { exitCode, stderr } = await build;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
@@ -2544,7 +2683,7 @@ test('deploy a Lambda with 3 seconds of maxDuration', async t => {
const url = new URL(output.stdout);
// Should time out
url.pathname = '/api/wait-for/4';
url.pathname = '/api/wait-for/5';
const response1 = await fetch(url.href);
t.is(
response1.status,
@@ -2553,7 +2692,7 @@ test('deploy a Lambda with 3 seconds of maxDuration', async t => {
);
// Should not time out
url.pathname = '/api/wait-for/2';
url.pathname = '/api/wait-for/1';
const response2 = await fetch(url.href);
t.is(
response2.status,
@@ -2683,59 +2822,10 @@ test('should show prompts to set up project during first deploy', async t => {
const now = execa(binaryPath, [dir, ...defaultArgs]);
await waitForPrompt(now, chunk => /Set up and deploy [^?]+\?/.test(chunk));
now.stdin.write('yes\n');
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Link to existing project?')
);
now.stdin.write('no\n');
await waitForPrompt(now, chunk =>
chunk.includes('Whats your projects name?')
);
now.stdin.write(`${projectName}\n`);
await waitForPrompt(now, chunk =>
chunk.includes('In which directory is your code located?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Want to override the settings?')
);
now.stdin.write('yes\n');
await waitForPrompt(now, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
now.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(now, chunk =>
chunk.includes(`What's your Build Command?`)
);
now.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(now, chunk =>
chunk.includes(`What's your Output Directory?`)
);
now.stdin.write(`o\n`);
await waitForPrompt(now, chunk =>
chunk.includes(`What's your Development Command?`)
);
now.stdin.write(`\n`);
await waitForPrompt(now, chunk => chunk.includes('Linked to'));
await setupProject(now, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
const output = await now;
@@ -3301,55 +3391,10 @@ test('[vc link] should show prompts to set up project', async t => {
const vc = execa(binaryPath, ['link', ...defaultArgs], { cwd: dir });
await waitForPrompt(vc, chunk => /Set up [^?]+\?/.test(chunk));
vc.stdin.write('yes\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Which scope should contain your project?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk => chunk.includes('Link to existing project?'));
vc.stdin.write('no\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Whats your projects name?')
);
vc.stdin.write(`${projectName}\n`);
await waitForPrompt(vc, chunk =>
chunk.includes('In which directory is your code located?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Want to override the settings?')
);
vc.stdin.write('yes\n');
await waitForPrompt(vc, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
vc.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Build Command?`)
);
vc.stdin.write(`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`);
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Output Directory?`)
);
vc.stdin.write(`o\n`);
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Development Command?`)
);
vc.stdin.write(`\n`);
await waitForPrompt(vc, chunk => chunk.includes('Linked to'));
await setupProject(vc, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
const output = await vc;
@@ -3408,6 +3453,29 @@ test('[vc link --confirm] should not show prompts and autolink', async t => {
);
});
test('[vc link] should not duplicate paths in .gitignore', async t => {
const dir = fixture('project-link-gitignore');
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--confirm', ...defaultArgs],
{ cwd: dir, reject: false }
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Ensure the message is correct pattern
t.regex(stderr, /Linked to /m);
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.output\n.vercel\n');
});
test('[vc dev] should show prompts to set up project', async t => {
const dir = fixture('project-link-dev');
const port = 58352;
@@ -3422,59 +3490,10 @@ test('[vc dev] should show prompts to set up project', async t => {
cwd: dir,
});
await waitForPrompt(dev, chunk => /Set up and develop [^?]+\?/.test(chunk));
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Which scope should contain your project?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Link to existing project?')
);
dev.stdin.write('no\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Whats your projects name?')
);
dev.stdin.write(`${projectName}\n`);
await waitForPrompt(dev, chunk =>
chunk.includes('In which directory is your code located?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Want to override the settings?')
);
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
dev.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Build Command?`)
);
dev.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Output Directory?`)
);
dev.stdin.write(`o\n`);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Development Command?`)
);
dev.stdin.write(`\n`);
await waitForPrompt(dev, chunk => chunk.includes('Linked to'));
await setupProject(dev, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
@@ -3574,59 +3593,12 @@ test('[vc dev] should send the platform proxy request headers to frontend dev se
cwd: dir,
});
await waitForPrompt(dev, chunk => /Set up and develop [^?]+\?/.test(chunk));
dev.stdin.write('yes\n');
await setupProject(dev, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
devCommand: 'node server.js',
});
await waitForPrompt(dev, chunk =>
chunk.includes('Which scope should contain your project?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Link to existing project?')
);
dev.stdin.write('no\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Whats your projects name?')
);
dev.stdin.write(`${projectName}\n`);
await waitForPrompt(dev, chunk =>
chunk.includes('In which directory is your code located?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Want to override the settings?')
);
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
dev.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Build Command?`)
);
dev.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Output Directory?`)
);
dev.stdin.write(`o\n`);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Development Command?`)
);
dev.stdin.write(`node server.js\n`);
await waitForPrompt(dev, chunk => chunk.includes('Linked to'));
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
// Ensure that `vc dev` also works

View File

@@ -374,4 +374,15 @@ describe('DevServer', () => {
);
})
);
it(
'should render an error page when the middleware returns not a Response',
testFixture('edge-middleware-invalid-response', async server => {
const response = await fetch(`${server.address}/index.html`);
const body = await response.text();
expect(body).toStrictEqual(
'A server error has occurred\n\nEDGE_FUNCTION_INVOCATION_FAILED\n'
);
})
);
});

View File

@@ -5,7 +5,7 @@ describe('getUpdateCommand', () => {
it('should detect update command', async () => {
const updateCommand = await getUpdateCommand();
expect(updateCommand).toEqual(
`yarn add vercel@${isCanary() ? 'canary' : 'latest'}`
`npm i vercel@${isCanary() ? 'canary' : 'latest'}`
);
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "10.2.3-canary.18",
"version": "10.2.3-canary.22",
"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.17",
"@vercel/build-utils": "2.12.3-canary.21",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

@@ -0,0 +1,25 @@
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="800" fill="#212121"/>
<g filter="url(#filter0_dd_126_53)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M587.947 527.768C592.201 582.418 592.201 608.036 592.201 636H465.756C465.756 629.909 465.865 624.337 465.975 618.687C466.317 601.123 466.674 582.807 463.828 545.819C460.067 491.667 436.748 479.634 393.871 479.634H355.883H195V381.109H399.889C454.049 381.109 481.13 364.633 481.13 321.011C481.13 282.654 454.049 259.41 399.889 259.41H195V163H422.456C545.069 163 606 220.912 606 313.42C606 382.613 563.123 427.739 505.201 435.26C554.096 445.037 582.681 472.865 587.947 527.768Z" fill="#E8F2FF"/>
<path d="M195 636V562.553H328.697C351.029 562.553 355.878 579.116 355.878 588.994V636H195Z" fill="#E8F2FF"/>
</g>
<defs>
<filter id="filter0_dd_126_53" x="131" y="99" width="539" height="601" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="28"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.223529 0 0 0 0 0.572549 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_126_53"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="32"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.223529 0 0 0 0 0.572549 0 0 0 0 1 0 0 0 0.9 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_126_53" result="effect2_dropShadow_126_53"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_126_53" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

@@ -43,6 +43,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `blitz build`',
value: 'blitz build',
},
devCommand: {
value: 'blitz start',
@@ -51,8 +52,6 @@ export const frameworks = [
placeholder: 'Next.js default',
},
},
devCommand: 'blitz start',
buildCommand: 'blitz build',
getFsOutputDir: async () => '.next',
getOutputDirName: async () => 'public',
},
@@ -83,6 +82,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `next build`',
value: 'next build',
},
devCommand: {
value: 'next dev --port $PORT',
@@ -98,8 +98,6 @@ export const frameworks = [
dependencies: ['next-plugin-sentry', 'next-sentry-source-maps'],
},
],
devCommand: 'next dev --port $PORT',
buildCommand: 'next build',
getFsOutputDir: async () => '.next',
getOutputDirName: async () => 'public',
cachePattern: '.next/cache/**',
@@ -114,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: [
@@ -131,6 +129,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `gatsby build`',
value: 'gatsby build',
},
devCommand: {
value: 'gatsby develop --port $PORT',
@@ -141,8 +140,6 @@ export const frameworks = [
},
},
dependency: 'gatsby',
devCommand: 'gatsby develop --port $PORT',
buildCommand: 'gatsby build',
getOutputDirName: async () => 'public',
getFsOutputDir: async () => 'public',
defaultRoutes: async (dirPrefix: string) => {
@@ -194,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.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',
@@ -203,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: [
{
@@ -219,6 +283,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `hexo generate`',
value: 'hexo generate',
},
devCommand: {
value: 'hexo server --port $PORT',
@@ -229,8 +294,6 @@ export const frameworks = [
},
},
dependency: 'hexo',
devCommand: 'hexo server --port $PORT',
buildCommand: 'hexo generate',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
},
@@ -243,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: [
{
@@ -259,6 +321,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `npx @11ty/eleventy`',
value: 'npx @11ty/eleventy',
},
devCommand: {
value: 'npx @11ty/eleventy --serve --watch --port $PORT',
@@ -269,8 +332,6 @@ export const frameworks = [
},
},
dependency: '@11ty/eleventy',
devCommand: 'npx @11ty/eleventy --serve --watch --port $PORT',
buildCommand: 'npx @11ty/eleventy',
getFsOutputDir: async () => '_site',
getOutputDirName: async () => '_site',
cachePattern: '.cache/**',
@@ -300,6 +361,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `docusaurus build`',
value: 'docusaurus build',
},
devCommand: {
value: 'docusaurus start --port $PORT',
@@ -310,8 +372,6 @@ export const frameworks = [
},
},
dependency: '@docusaurus/core',
devCommand: 'docusaurus start --port $PORT',
buildCommand: 'docusaurus build',
getFsOutputDir: async (dirPrefix: string) => {
const base = 'build';
try {
@@ -456,6 +516,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `docusaurus-build`',
value: 'docusaurus-build',
},
devCommand: {
value: 'docusaurus-start --port $PORT',
@@ -466,8 +527,6 @@ export const frameworks = [
},
},
dependency: 'docusaurus',
devCommand: 'docusaurus-start --port $PORT',
buildCommand: 'docusaurus-build',
getFsOutputDir: async (dirPrefix: string) => {
const base = 'build';
try {
@@ -523,6 +582,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `preact build`',
value: 'preact build',
},
devCommand: {
value: 'preact watch --port $PORT',
@@ -533,8 +593,6 @@ export const frameworks = [
},
},
dependency: 'preact-cli',
devCommand: 'preact watch --port $PORT',
buildCommand: 'preact build',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build',
defaultRoutes: [
@@ -581,6 +639,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `dojo build`',
value: 'dojo build',
},
devCommand: {
value: 'dojo build -m dev -w -s -p $PORT',
@@ -591,8 +650,6 @@ export const frameworks = [
},
},
dependency: '@dojo/cli',
devCommand: 'dojo build -m dev -w -s -p $PORT',
buildCommand: 'dojo build',
getFsOutputDir: async () => 'output/dist',
getOutputDirName: async () => join('output', 'dist'),
defaultRoutes: [
@@ -649,6 +706,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `ember build`',
value: 'ember build',
},
devCommand: {
value: 'ember serve --port $PORT',
@@ -659,8 +717,6 @@ export const frameworks = [
},
},
dependency: 'ember-cli',
devCommand: 'ember serve --port $PORT',
buildCommand: 'ember build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
defaultRoutes: [
@@ -705,6 +761,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `vue-cli-service build`',
value: 'vue-cli-service build',
},
devCommand: {
value: 'vue-cli-service serve --port $PORT',
@@ -715,8 +772,6 @@ export const frameworks = [
},
},
dependency: '@vue/cli-service',
devCommand: 'vue-cli-service serve --port $PORT',
buildCommand: 'vue-cli-service build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
defaultRoutes: [
@@ -783,6 +838,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `ng build && scully`',
value: 'ng build && scully',
},
devCommand: {
value: 'ng serve --port $PORT',
@@ -793,8 +849,6 @@ export const frameworks = [
},
},
dependency: '@scullyio/init',
devCommand: 'ng serve --port $PORT',
buildCommand: 'ng build && scully',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist/static',
},
@@ -822,6 +876,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `ng build`',
value: 'ng build',
},
devCommand: {
value: 'ng serve --port $PORT',
@@ -831,8 +886,6 @@ export const frameworks = [
},
},
dependency: '@ionic/angular',
devCommand: 'ng serve --port $PORT',
buildCommand: 'ng build',
getFsOutputDir: async () => 'www',
getOutputDirName: async () => 'www',
defaultRoutes: [
@@ -876,6 +929,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `ng build`',
value: 'ng build',
},
devCommand: {
value: 'ng serve --port $PORT',
@@ -886,8 +940,6 @@ export const frameworks = [
},
},
dependency: '@angular/cli',
devCommand: 'ng serve --port $PORT',
buildCommand: 'ng build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async (dirPrefix: string) => {
const base = 'dist';
@@ -945,6 +997,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `polymer build`',
value: 'polymer build',
},
devCommand: {
value: 'polymer serve --port $PORT',
@@ -955,8 +1008,6 @@ export const frameworks = [
},
},
dependency: 'polymer-cli',
devCommand: 'polymer serve --port $PORT',
buildCommand: 'polymer build',
getFsOutputDir: async () => 'build',
getOutputDirName: async (dirPrefix: string) => {
const base = 'build';
@@ -996,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: [
{
@@ -1016,6 +1068,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `rollup -c`',
value: 'rollup -c',
},
devCommand: {
value: 'rollup -c -w',
@@ -1025,8 +1078,6 @@ export const frameworks = [
},
},
dependency: 'sirv-cli',
devCommand: 'rollup -c -w',
buildCommand: 'rollup -c',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
defaultRoutes: [
@@ -1070,6 +1121,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `svelte-kit build`',
value: 'svelte-kit build',
},
devCommand: {
value: 'svelte-kit dev --port $PORT',
@@ -1079,8 +1131,6 @@ export const frameworks = [
placeholder: 'public',
},
},
devCommand: 'svelte-kit dev --port $PORT',
buildCommand: 'svelte-kit build',
getFsOutputDir: async () => '.output',
getOutputDirName: async () => 'public',
},
@@ -1108,6 +1158,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `react-scripts build`',
value: 'react-scripts build',
},
devCommand: {
value: 'react-scripts start',
@@ -1117,8 +1168,6 @@ export const frameworks = [
},
},
dependency: '@ionic/react',
devCommand: 'react-scripts start',
buildCommand: 'react-scripts build',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build',
defaultRoutes: [
@@ -1195,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: [
@@ -1216,6 +1266,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `react-scripts build`',
value: 'react-scripts build',
},
devCommand: {
value: 'react-scripts start',
@@ -1225,8 +1276,6 @@ export const frameworks = [
},
},
dependency: 'react-scripts',
devCommand: 'react-scripts start',
buildCommand: 'react-scripts build',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build',
defaultRoutes: [
@@ -1318,6 +1367,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `gridsome build`',
value: 'gridsome build',
},
devCommand: {
value: 'gridsome develop -p $PORT',
@@ -1328,8 +1378,6 @@ export const frameworks = [
},
},
dependency: 'gridsome',
devCommand: 'gridsome develop -p $PORT',
buildCommand: 'gridsome build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
},
@@ -1357,6 +1405,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `umi build`',
value: 'umi build',
},
devCommand: {
value: 'umi dev --port $PORT',
@@ -1367,8 +1416,6 @@ export const frameworks = [
},
},
dependency: 'umi',
devCommand: 'umi dev --port $PORT',
buildCommand: 'umi build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
defaultRoutes: [
@@ -1412,6 +1459,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `sapper export`',
value: 'sapper export',
},
devCommand: {
value: 'sapper dev --port $PORT',
@@ -1422,8 +1470,6 @@ export const frameworks = [
},
},
dependency: 'sapper',
devCommand: 'sapper dev --port $PORT',
buildCommand: 'sapper export',
getFsOutputDir: async () => '__sapper__/export',
getOutputDirName: async () => '__sapper__/export',
},
@@ -1451,6 +1497,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `saber build`',
value: 'saber build',
},
devCommand: {
value: 'saber --port $PORT',
@@ -1461,8 +1508,6 @@ export const frameworks = [
},
},
dependency: 'saber',
devCommand: 'saber --port $PORT',
buildCommand: 'saber build',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
defaultRoutes: [
@@ -1521,6 +1566,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `stencil build`',
value: 'stencil build',
},
devCommand: {
value: 'stencil build --dev --watch --serve --port $PORT',
@@ -1531,8 +1577,6 @@ export const frameworks = [
},
},
dependency: '@stencil/core',
devCommand: 'stencil build --dev --watch --serve --port $PORT',
buildCommand: 'stencil build',
getFsOutputDir: async () => 'www',
getOutputDirName: async () => 'www',
defaultRoutes: [
@@ -1595,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: [
@@ -1611,6 +1656,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `nuxt generate`',
value: 'nuxt generate',
},
devCommand: {
value: 'nuxt',
@@ -1620,8 +1666,6 @@ export const frameworks = [
},
},
dependency: 'nuxt',
devCommand: 'nuxt',
buildCommand: 'nuxt generate',
getFsOutputDir: async () => '.output',
getOutputDirName: async () => 'dist',
cachePattern: '.nuxt/**',
@@ -1680,8 +1724,6 @@ export const frameworks = [
placeholder: 'RedwoodJS default',
},
},
devCommand: 'yarn rw dev --fwd="--port=$PORT --open=false',
buildCommand: 'yarn rw deploy vercel',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
},
@@ -1694,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: [
{
@@ -1717,6 +1758,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `hugo -D --gc`',
value: 'hugo -D --gc',
},
devCommand: {
value: 'hugo server -D -w -p $PORT',
@@ -1726,8 +1768,6 @@ export const frameworks = [
placeholder: '`public` or `publishDir` from the `config` file',
},
},
devCommand: 'hugo server -D -w -p $PORT',
buildCommand: 'hugo -D --gc',
getFsOutputDir: async (dirPrefix: string): Promise<string> => {
type HugoConfig = { publishDir?: string };
const config = await readConfigFile<HugoConfig>(
@@ -1772,6 +1812,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `jekyll build`',
value: 'jekyll build',
},
devCommand: {
value: 'bundle exec jekyll serve --watch --port $PORT',
@@ -1781,8 +1822,6 @@ export const frameworks = [
placeholder: '`_site` or `destination` from `_config.yml`',
},
},
devCommand: 'bundle exec jekyll serve --watch --port $PORT',
buildCommand: 'jekyll build',
getFsOutputDir: async (dirPrefix: string): Promise<string> => {
type JekyllConfig = { destination?: string };
const config = await readConfigFile<JekyllConfig>(
@@ -1821,6 +1860,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `brunch build --production`',
value: 'brunch build --production',
},
devCommand: {
value: 'brunch watch --server --port $PORT',
@@ -1830,8 +1870,6 @@ export const frameworks = [
value: 'public',
},
},
devCommand: 'brunch watch --server --port $PORT',
buildCommand: 'brunch build --production',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
},
@@ -1856,18 +1894,17 @@ export const frameworks = [
value: 'bundle install',
},
buildCommand: {
value: '`npm run build` or `bundle exec middleman build`',
placeholder: '`npm run build` or `bundle exec middleman build`',
value: 'bundle exec middleman build',
},
devCommand: {
value: 'bundle exec middleman server -p $PORT',
placeholder: 'bundle exec middleman server',
value: 'bundle exec middleman server -p $PORT',
},
outputDirectory: {
value: 'build',
},
},
devCommand: 'bundle exec middleman server -p $PORT',
buildCommand: 'bundle exec middleman build',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build',
cachePattern: '{vendor/bin,vendor/cache,vendor/bundle}/**',
@@ -1896,15 +1933,13 @@ export const frameworks = [
value: 'zola build',
},
devCommand: {
value: 'zola serve --port $PORT',
placeholder: 'zola serve',
value: 'zola serve --port $PORT',
},
outputDirectory: {
value: 'public',
},
},
devCommand: 'zola serve --port $PORT',
buildCommand: 'zola build',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
defaultVersion: '0.13.0',
@@ -1934,17 +1969,17 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `vite build`',
value: 'vite build',
},
devCommand: {
placeholder: 'vite',
value: 'vite',
},
outputDirectory: {
value: 'dist',
},
},
dependency: 'vite',
devCommand: 'vite',
buildCommand: 'vite build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
},
@@ -1972,17 +2007,17 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `parcel build`',
value: 'parcel build',
},
devCommand: {
placeholder: 'parcel',
value: 'parcel',
},
outputDirectory: {
placeholder: 'dist',
value: 'dist',
},
},
dependency: 'parcel',
devCommand: 'parcel',
buildCommand: 'parcel build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
defaultRoutes: [
@@ -2016,16 +2051,16 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run vercel-build` or `npm run build`',
value: null,
},
devCommand: {
placeholder: 'None',
value: null,
},
outputDirectory: {
placeholder: '`public` if it exists, or `.`',
},
},
devCommand: null,
buildCommand: null,
getFsOutputDir: async (dirPrefix: string): Promise<string> => {
// Public if it exists or `.`
let base = 'public';

View File

@@ -26,7 +26,7 @@ export interface SettingValue {
* A predefined setting for the detected framework
* @example "next dev --port $PORT"
*/
value: string;
value: string | null;
placeholder?: string;
}
@@ -129,11 +129,11 @@ export interface Framework {
/**
* Default Build Command or a placeholder
*/
buildCommand: Setting;
buildCommand: SettingValue;
/**
* Default Development Command or a placeholder
*/
devCommand: Setting;
devCommand: SettingValue;
/**
* Default Output Directory
*/
@@ -157,6 +157,7 @@ export interface Framework {
/**
* Name of a dependency in `package.json` to detect this framework.
* @example "hexo"
* @deprecated use `detectors` instead (new frameworks should not use this prop)
*/
dependency?: string;
/**
@@ -201,16 +202,6 @@ export interface Framework {
* @example ".cache/**"
*/
cachePattern?: string;
/**
* The default build command for the framework.
* @example "next build"
*/
buildCommand: string | null;
/**
* The default development command for the framework.
* @example "next dev"
*/
devCommand: string | null;
/**
* The default version of the framework command that is available within the
* build image. Usually an environment variable can be set to override this.

View File

@@ -34,7 +34,7 @@ const SchemaSettings = {
additionalProperties: false,
properties: {
value: {
type: 'string',
type: ['string', 'null'],
},
placeholder: {
type: 'string',
@@ -58,15 +58,7 @@ const Schema = {
type: 'array',
items: {
type: 'object',
required: [
'name',
'slug',
'logo',
'description',
'settings',
'buildCommand',
'devCommand',
],
required: ['name', 'slug', 'logo', 'description', 'settings'],
properties: {
name: { type: 'string' },
slug: { type: ['string', 'null'] },
@@ -138,8 +130,6 @@ const Schema = {
dependency: { type: 'string' },
cachePattern: { type: 'string' },
buildCommand: { type: ['string', 'null'] },
devCommand: { type: ['string', 'null'] },
defaultVersion: { type: 'string' },
},
},

View File

@@ -6,7 +6,8 @@ import { join } from 'path';
import stringArgv from 'string-argv';
import { debug } from '@vercel/build-utils';
const versionMap = new Map([
['1.16', '1.16'],
['1.17', '1.17.3'],
['1.16', '1.16.10'],
['1.15', '1.15.8'],
['1.14', '1.14.15'],
['1.13', '1.13.15'],

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/go",
"version": "1.2.4-canary.3",
"version": "1.2.4-canary.4",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",

View File

@@ -5,10 +5,10 @@
{ "src": "subdirectory/index.go", "use": "@vercel/go" }
],
"probes": [
{ "path": "/", "mustContain": "cow:go1.16:RANDOMNESS_PLACEHOLDER" },
{ "path": "/", "mustContain": "cow:go1.17.3:RANDOMNESS_PLACEHOLDER" },
{
"path": "/subdirectory",
"mustContain": "subcow:go1.16:RANDOMNESS_PLACEHOLDER"
"mustContain": "subcow:go1.17.3:RANDOMNESS_PLACEHOLDER"
}
]
}

View File

@@ -1,3 +1,3 @@
module with-nested
go 1.12
go 1.16

View File

@@ -1,6 +1,6 @@
{
"name": "vercel-plugin-middleware",
"version": "0.0.0-canary.6",
"version": "0.0.0-canary.7",
"license": "MIT",
"main": "./dist/index",
"homepage": "",

View File

@@ -3,13 +3,16 @@ _ENTRIES = typeof _ENTRIES === 'undefined' ? {} : _ENTRIES;
_ENTRIES['middleware_pages/_middleware'] = {
default: async function (ev) {
const result = await middleware.default(ev.request, ev);
if (result instanceof Response) {
return {
promise: Promise.resolve(),
waitUntil: Promise.resolve(),
response: result,
};
}
return result;
return {
promise: Promise.resolve(),
waitUntil: Promise.resolve(),
response:
result ||
new Response(null, {
headers: {
'x-middleware-next': 1,
},
}),
};
},
};

View File

@@ -1,8 +1,19 @@
import { join } from 'path';
import { promises as fsp } from 'fs';
import { build } from '../src';
import { Response } from 'node-fetch';
describe('build()', () => {
beforeEach(() => {
//@ts-ignore
global.Response = Response;
});
afterEach(() => {
//@ts-ignore
delete global.Response;
//@ts-ignore
delete global._ENTRIES;
});
it('should build simple middleware', async () => {
const fixture = join(__dirname, 'fixtures/simple');
await build({
@@ -17,8 +28,30 @@ describe('build()', () => {
);
expect(middlewareManifest).toMatchSnapshot();
const outputFile = join(fixture, '.output/server/pages/_middleware.js');
expect(await fsp.stat(outputFile)).toBeTruthy();
require(outputFile);
//@ts-ignore
const middleware = global._ENTRIES['middleware_pages/_middleware'].default;
expect(typeof middleware).toStrictEqual('function');
const handledResponse = await middleware({
request: {
url: 'http://google.com',
},
});
const unhandledResponse = await middleware({
request: {
url: 'literallyanythingelse',
},
});
expect(String(handledResponse.response.body)).toEqual('Hi from the edge!');
expect(
await fsp.stat(join(fixture, '.output/server/pages/_middleware.js'))
).toBeTruthy();
(handledResponse.response as Response).headers.get('x-middleware-next')
).toEqual(null);
expect(unhandledResponse.response.body).toEqual(null);
expect(
(unhandledResponse.response as Response).headers.get('x-middleware-next')
).toEqual('1');
});
});

View File

@@ -1 +1,5 @@
export default () => new Response('Hi from the edge!');
export default req => {
if (req.url === 'http://google.com') {
return new Response('Hi from the edge!');
}
};

View File

@@ -33,14 +33,6 @@ async function main() {
await fs.remove(symlinkTarget);
await fs.symlink('symlinked-asset', symlinkTarget);
// Use types.d.ts as the main types export
await Promise.all(
(await fs.readdir(outDir))
.filter(p => p.endsWith('.d.ts') && p !== 'types.d.ts')
.map(p => fs.remove(join(outDir, p)))
);
await fs.rename(join(outDir, 'types.d.ts'), join(outDir, 'index.d.ts'));
// Bundle helpers.ts with ncc
await fs.remove(join(outDir, 'helpers.js'));
const helpersDir = join(outDir, 'helpers');

View File

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

View File

@@ -339,6 +339,9 @@ function getAWSLambdaHandler(entrypoint: string, config: Config) {
return '';
}
// Ensures that everything from `types.ts` is exported in the final `index.d.ts` file.
export * from './types';
export const version = 3;
export async function build({

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "vercel-plugin-node",
"version": "1.12.2-canary.7",
"version": "1.12.2-canary.12",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -33,7 +33,9 @@
"@types/jest": "27.0.2",
"@types/node-fetch": "2",
"@types/test-listen": "1.1.0",
"@vercel/fun": "1.0.1",
"@types/yazl": "2.4.2",
"@vercel/build-utils": "2.12.3-canary.21",
"@vercel/fun": "1.0.3",
"@vercel/ncc": "0.24.0",
"@vercel/nft": "0.14.0",
"@vercel/node-bridge": "2.1.1-canary.2",
@@ -46,7 +48,8 @@
"node-fetch": "2",
"source-map-support": "0.5.12",
"test-listen": "1.1.0",
"ts-morph": "12.0.0"
"ts-morph": "12.0.0",
"yazl": "2.5.1"
},
"jest": {
"preset": "ts-jest",

View File

@@ -35,7 +35,11 @@ import {
debug,
isSymbolicLink,
runNpmInstall,
updateFunctionsManifest,
updateRoutesManifest,
walkParentDirs,
normalizePath,
runPackageJsonScript,
} from '@vercel/build-utils';
import { FromSchema } from 'json-schema-to-ts';
import { getConfig, BaseFunctionConfigSchema } from '@vercel/static-config';
@@ -43,6 +47,8 @@ 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';
import type { VercelConfig } from '@vercel/client';
export { shouldServe };
export {
@@ -64,6 +70,7 @@ const { makeVercelLauncher, makeAwsLauncher } = require_(
interface DownloadOptions {
entrypoint: string;
workPath: string;
installedPaths?: Set<string>;
}
interface PortInfo {
@@ -109,17 +116,47 @@ const BRIDGE_FILENAME = '__bridge.js';
const HELPERS_FILENAME = '__helpers.js';
const SOURCEMAP_SUPPORT_FILENAME = '__sourcemap_support.js';
async function downloadInstallAndBundle({
async function maybeInstallAndBuild({
entrypoint,
workPath,
installedPaths,
}: DownloadOptions) {
const entrypointFsDirname = join(workPath, dirname(entrypoint));
const nodeVersion = await getNodeVersion(entrypointFsDirname);
const spawnOpts = getSpawnOptions({}, nodeVersion);
const installTime = Date.now();
await runNpmInstall(entrypointFsDirname, [], spawnOpts, {}, nodeVersion);
debug(`Install complete [${Date.now() - installTime}ms]`);
const lastPath = await walkParentDirs({
base: workPath,
start: entrypointFsDirname,
filename: 'package.json',
});
if (!lastPath || dirname(lastPath) === workPath) {
debug(`Skip install command in \`vercel-plugin-node\` for ${entrypoint}.`);
} else if (lastPath) {
if (!installedPaths?.has(lastPath)) {
installedPaths?.add(lastPath);
const installTime = Date.now();
await runNpmInstall(dirname(lastPath), [], spawnOpts, {}, nodeVersion);
debug(
`Install complete [${Date.now() - installTime}ms] for ${relative(
workPath,
lastPath
)}`
);
await runPackageJsonScript(
dirname(lastPath),
// Don't consider "build" script since its intended for frontend code
['vercel-build', 'now-build'],
spawnOpts
);
} else {
debug(
`Skip install command in \`vercel-plugin-node\` for ${entrypoint}. Already installed for other entrypoint.`
);
}
}
return {
nodeVersion,
@@ -343,9 +380,16 @@ function getAWSLambdaHandler(entrypoint: string, config: FunctionConfig) {
}
// TODO NATE: turn this into a `@vercel/plugin-utils` helper function?
export async function build({ workPath }: { workPath: string }) {
export async function build({
vercelConfig,
workPath,
}: {
vercelConfig: VercelConfig;
workPath: string;
}) {
const project = new Project();
const entrypoints = await glob('api/**/*.[jt]s', workPath);
const installedPaths = new Set<string>();
for (const entrypoint of Object.keys(entrypoints)) {
// Dotfiles are not compiled
if (entrypoint.includes('/.')) continue;
@@ -370,54 +414,59 @@ export async function build({ workPath }: { workPath: string }) {
continue;
}
await buildEntrypoint({ workPath, entrypoint, config });
await buildEntrypoint({
vercelConfig,
workPath,
entrypoint,
config,
installedPaths,
});
}
}
export async function buildEntrypoint({
vercelConfig,
workPath,
entrypoint,
config,
installedPaths,
}: {
vercelConfig: VercelConfig;
workPath: string;
entrypoint: string;
config: FunctionConfig;
installedPaths?: Set<string>;
}) {
// Unique hash that will be used as directory name for `.output`.
const entrypointHash = crypto
.createHash('sha256')
.update(entrypoint)
.digest('hex');
const outputDirPath = join(workPath, '.output');
const { dir, name } = parsePath(entrypoint);
const entrypointWithoutExt = join('/', dir, name);
const entrypointWithoutExtIndex = join(
dir,
name,
name === 'index' ? '' : 'index'
);
const outputWorkPath = join(
outputDirPath,
'server/pages',
entrypointWithoutExtIndex
);
const outputWorkPath = join(outputDirPath, 'inputs', entrypointHash);
const pagesDir = join(outputDirPath, 'server', 'pages');
const pageOutput = join(pagesDir, renameTStoJS(entrypoint));
const nftOutput = `${pageOutput}.nft.json`;
await fsp.mkdir(outputWorkPath, { recursive: true });
await fsp.mkdir(parsePath(pageOutput).dir, { recursive: true });
console.log(`Compiling "${entrypoint}" to "${outputWorkPath}"`);
const shouldAddHelpers =
config.helpers !== false && process.env.NODEJS_HELPERS !== '0';
const awsLambdaHandler = getAWSLambdaHandler(entrypoint, config);
const { nodeVersion } = await downloadInstallAndBundle({
const { nodeVersion } = await maybeInstallAndBuild({
entrypoint,
workPath,
installedPaths,
});
const entrypointPath = join(workPath, entrypoint);
// TODO NATE: do we want to run the build script?
// The frontend build command probably already did this
//await runPackageJsonScript(
// entrypointFsDirname,
// // Don't consider "build" script since its intended for frontend code
// ['vercel-build', 'now-build'],
// spawnOpts
//);
debug('Tracing input files...');
const traceTime = Date.now();
const { preparedFiles, shouldAddSourcemapSupport } = await compile(
@@ -427,11 +476,12 @@ export async function buildEntrypoint({
);
debug(`Trace complete [${Date.now() - traceTime}ms]`);
const getFileName = (str: string) => `___vc/${str}`;
// Has to be in `dirname(entrypoint)` because the `handler` will be prefixed with this path.
const getVCFileName = (str: string) => `${dirname(entrypoint)}/___vc/${str}`;
const launcher = awsLambdaHandler ? makeAwsLauncher : makeVercelLauncher;
const launcherSource = launcher({
entrypointPath: `../${renameTStoJS(relative(workPath, entrypointPath))}`,
entrypointPath: `../${renameTStoJS(basename(entrypoint))}`,
bridgePath: `./${BRIDGE_FILENAME}`,
helpersPath: `./${HELPERS_FILENAME}`,
sourcemapSupportPath: `./${SOURCEMAP_SUPPORT_FILENAME}`,
@@ -441,25 +491,25 @@ export async function buildEntrypoint({
});
const launcherFiles: Files = {
[getFileName('package.json')]: new FileBlob({
[getVCFileName('package.json')]: new FileBlob({
data: JSON.stringify({ type: 'commonjs' }),
}),
[getFileName(LAUNCHER_FILENAME)]: new FileBlob({
[getVCFileName(LAUNCHER_FILENAME)]: new FileBlob({
data: launcherSource,
}),
[getFileName(BRIDGE_FILENAME)]: new FileFsRef({
[getVCFileName(BRIDGE_FILENAME)]: new FileFsRef({
fsPath: join(DIST_DIR, 'bridge.js'),
}),
};
if (shouldAddSourcemapSupport) {
launcherFiles[getFileName(SOURCEMAP_SUPPORT_FILENAME)] = new FileFsRef({
launcherFiles[getVCFileName(SOURCEMAP_SUPPORT_FILENAME)] = new FileFsRef({
fsPath: join(DIST_DIR, 'source-map-support.js'),
});
}
if (shouldAddHelpers) {
launcherFiles[getFileName(HELPERS_FILENAME)] = new FileFsRef({
launcherFiles[getVCFileName(HELPERS_FILENAME)] = new FileFsRef({
fsPath: join(DIST_DIR, 'helpers.js'),
});
}
@@ -470,6 +520,8 @@ export async function buildEntrypoint({
...launcherFiles,
};
const nftFiles: { input: string; output: string }[] = [];
for (const filename of Object.keys(files)) {
const outPath = join(outputWorkPath, filename);
const file = files[filename];
@@ -480,48 +532,44 @@ export async function buildEntrypoint({
const finishPromise = once(ws, 'finish');
file.toStream().pipe(ws);
await finishPromise;
// The `handler` will be `.output/server/pages/api/subdirectory/___vc/__launcher.launcher`
// or `.output/server/pages/api/___vc/__launcher.launcher`.
// This means everything has to be mounted to the `dirname` of the entrypoint.
nftFiles.push({
input: relative(dirname(nftOutput), outPath),
output: join('.output', 'server', 'pages', filename),
});
}
// Update the `functions-mainifest.json` file with this entrypoint
// TODO NATE: turn this into a `@vercel/plugin-utils` helper function?
const functionsManifestPath = join(outputDirPath, 'functions-manifest.json');
let functionsManifest: any = {};
try {
functionsManifest = JSON.parse(
await fsp.readFile(functionsManifestPath, 'utf8')
);
} catch (_err) {
// ignore...
}
if (!functionsManifest.version) functionsManifest.version = 1;
if (!functionsManifest.pages) functionsManifest.pages = {};
functionsManifest.pages[entrypointWithoutExtIndex] = {
handler: `${getFileName(LAUNCHER_FILENAME).slice(0, -3)}.launcher`,
runtime: nodeVersion.runtime,
};
await fsp.writeFile(
functionsManifestPath,
JSON.stringify(functionsManifest, null, 2)
nftOutput,
JSON.stringify({
version: 1,
files: nftFiles,
})
);
await fsp.copyFile(
join(outputWorkPath, renameTStoJS(entrypoint)),
pageOutput
);
const pages = {
[normalizePath(relative(pagesDir, pageOutput))]: {
handler: `___vc/${LAUNCHER_FILENAME.slice(0, -3)}.launcher`,
runtime: nodeVersion.runtime,
},
};
await updateFunctionsManifest({ vercelConfig, workPath, pages });
// Update the `routes-mainifest.json` file with the wildcard route
// when the entrypoint is dynamic (i.e. `/api/[id].ts`).
if (isDynamicRoute(entrypointWithoutExt)) {
const routesManifestPath = join(outputDirPath, 'routes-manifest.json');
let routesManifest: any = {};
try {
routesManifest = JSON.parse(
await fsp.readFile(routesManifestPath, 'utf8')
);
} catch (_err) {
// ignore...
}
if (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = [];
routesManifest.dynamicRoutes.push(pageToRoute(entrypointWithoutExt));
await fsp.writeFile(
routesManifestPath,
JSON.stringify(routesManifest, null, 2)
);
await updateRoutesManifest({
workPath,
dynamicRoutes: [pageToRoute(entrypointWithoutExt)],
});
}
}

View File

@@ -1,6 +1,7 @@
import { join } from 'path';
import path from 'path';
import { parse } from 'url';
import { promises as fsp } from 'fs';
import { ZipFile } from 'yazl';
import { createFunction, Lambda } from '@vercel/fun';
import {
Request,
@@ -11,6 +12,7 @@ import {
Headers,
} from 'node-fetch';
import { build } from '../src';
import { runNpmInstall, streamToBuffer } from '@vercel/build-utils';
interface TestParams {
fixture: string;
@@ -50,40 +52,66 @@ function withFixture<T>(
t: (props: TestParams) => Promise<T>
): () => Promise<T> {
return async () => {
const fixture = join(__dirname, 'fixtures', name);
const fixture = path.join(__dirname, 'fixtures', name);
await fsp.rmdir(path.join(fixture, '.output'), { recursive: true });
const functions = new Map<string, Lambda>();
async function fetch(r: RequestInfo, init?: RequestInit) {
const req = new Request(r, init);
const url = parse(req.url);
const pathWithIndex = join(
url.pathname!,
url.pathname!.endsWith('/index') ? '' : 'index'
).substring(1);
const functionPath = url.pathname!.substring(1);
let status = 404;
let headers: HeadersInit = {};
let body: string | Buffer = 'Function not found';
let fn = functions.get(pathWithIndex);
let fn = functions.get(functionPath);
if (!fn) {
const manifest = JSON.parse(
await fsp.readFile(
join(fixture, '.output/functions-manifest.json'),
path.join(fixture, '.output/functions-manifest.json'),
'utf8'
)
);
const functionManifest = manifest.pages[pathWithIndex];
const keyFile = `${functionPath}.js`;
const keyIndex = `${functionPath}/index.js`;
const fnKey = keyFile in manifest.pages ? keyFile : keyIndex;
const functionManifest = manifest.pages[fnKey];
if (functionManifest) {
const dir = join(fixture, '.output/server/pages', pathWithIndex);
const entry = path.join(fixture, '.output/server/pages', fnKey);
const nftFile = JSON.parse(
await fsp.readFile(`${entry}.nft.json`, 'utf8')
);
const zip = new ZipFile();
zip.addFile(
path.join(fixture, '.output/server/pages', fnKey),
path.join('.output/server/pages', fnKey)
);
nftFile.files.forEach((f: { input: string; output: string }) => {
const input = path.join(path.dirname(entry), f.input);
zip.addFile(input, f.output);
});
zip.end();
const handler = path.posix.join(
'.output/server/pages',
path.dirname(fnKey),
functionManifest.handler
);
fn = await createFunction({
Code: {
Directory: dir,
ZipFile: await streamToBuffer(zip.outputStream),
},
Handler: functionManifest.handler,
Handler: handler,
Runtime: functionManifest.runtime,
});
functions.set(pathWithIndex, fn);
functions.set(functionPath, fn);
}
}
@@ -109,7 +137,22 @@ function withFixture<T>(
});
}
await build({ workPath: fixture });
if (
await fsp.lstat(path.join(fixture, 'package.json')).catch(() => false)
) {
await runNpmInstall(fixture);
}
let vercelConfig = {};
try {
vercelConfig = JSON.parse(
await fsp.readFile(path.join(fixture, 'vercel.json'), 'utf8')
);
} catch (e) {
// Consume error
}
await build({ vercelConfig, workPath: fixture });
try {
return await t({ fixture, fetch });
@@ -316,4 +359,28 @@ describe('build()', () => {
expect(body3.includes('GraphQL Playground')).toEqual(true);
})
);
it(
'should build "nested-lock-and-build"',
withFixture('nested-lock-and-build', async ({ fetch }) => {
const resA = await fetch('/api/users/[id]');
expect(resA.headers.get('x-date')).toEqual('2021-11-20T20:00:00.000Z');
const body = await resA.text();
expect(body).toEqual(
' _______________________________\n' +
'< Hello from /api/users/[id].js >\n' +
' -------------------------------\n' +
' \\ ^__^\n' +
' \\ (oo)\\_______\n' +
' (__)\\ )\\/\\\n' +
' ||----w |\n' +
' || ||'
);
const resB = await fetch('/api/profile');
expect(await resB.text()).toEqual('true');
})
);
});

View File

@@ -0,0 +1,8 @@
{
"scripts": {
"vercel-build": "node ./script.js"
},
"dependencies": {
"cowsay": "1.5.0"
}
}

View File

@@ -0,0 +1,9 @@
const isSvg = require('is-svg');
module.exports = (req, res) => {
res.end(
isSvg(
'<svg xmlns="http://www.w3.org/2000/svg"><path fill="#00CD9F"/></svg>'
).toString()
);
};

View File

@@ -0,0 +1,11 @@
const fs = require('fs');
const path = require('path');
fs.writeFileSync(
path.join(__dirname, 'users', '.date.js'),
`
module.exports = {
date: '2021-11-20T20:00:00.000Z'
};
`.trim()
);

View File

@@ -0,0 +1,11 @@
const cowsay = require('cowsay');
const { date } = require('./.date');
module.exports = (req, res) => {
res.setHeader('x-date', date);
res.end(
cowsay.say({
text: 'Hello from /api/users/[id].js',
})
);
};

View File

@@ -0,0 +1,220 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
cliui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
cowsay@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/cowsay/-/cowsay-1.5.0.tgz#4a2a453b8b59383c7d7a50e44d765c5de0bf615f"
integrity sha512-8Ipzr54Z8zROr/62C8f0PdhQcDusS05gKTS87xxdji8VbWefWly0k8BwGK7+VqamOrkv3eGsCkPtvlHzrhWsCA==
dependencies:
get-stdin "8.0.0"
string-width "~2.1.1"
strip-final-newline "2.0.0"
yargs "15.4.1"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
dependencies:
locate-path "^5.0.0"
path-exists "^4.0.0"
get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-stdin@8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
dependencies:
p-locate "^4.1.0"
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
dependencies:
p-try "^2.0.0"
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
p-limit "^2.2.0"
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@~2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
dependencies:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
strip-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
dependencies:
ansi-regex "^3.0.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-final-newline@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs@15.4.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.2"

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"is-svg": "4.3.1"
}
}

View File

@@ -0,0 +1,22 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
fast-xml-parser@^3.19.0:
version "3.21.1"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz#152a1d51d445380f7046b304672dd55d15c9e736"
integrity sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==
dependencies:
strnum "^1.0.4"
is-svg@4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.1.tgz#8c63ec8c67c8c7f0a8de0a71c8c7d58eccf4406b"
integrity sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==
dependencies:
fast-xml-parser "^3.19.0"
strnum@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.4.tgz#e97e36a7d6ba9f93d0d6b496b2ed0678d422832b"
integrity sha512-lMzNMfDpaQOLt4B2mEbfzYS0+T7dvCXeojnlGf6f1AygvWDMcWyXYaLbyICfjVu29sErR8fnRagQfBW/N/hGgw==

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import {
createLambda,
shouldServe,
debug,
NowBuildError,
} from '@vercel/build-utils';
import { installRequirement, installRequirementsFile } from './install';
@@ -22,6 +23,7 @@ async function pipenvConvert(cmd: string, srcDir: string) {
const out = await execa.stdout(cmd, [], {
cwd: srcDir,
});
debug('Contents of requirements.txt is: ' + out);
fs.writeFileSync(join(srcDir, 'requirements.txt'), out);
} catch (err) {
console.log('Failed to run "pipfile2req"');
@@ -57,6 +59,10 @@ export const build = async ({
meta = {},
config,
}: BuildOptions) => {
let pipPath = meta.isDev ? 'pip3' : 'pip3.9';
let pythonPath = meta.isDev ? 'python3' : 'python3.9';
let pythonRuntime = meta.isDev ? 'python3' : 'python3.9';
workPath = await downloadFilesInWorkPath({
workPath,
files: originalFiles,
@@ -85,6 +91,8 @@ export const build = async ({
console.log('Installing required dependencies...');
await installRequirement({
pythonPath,
pipPath,
dependency: 'werkzeug',
version: '1.0.1',
workPath,
@@ -103,12 +111,43 @@ export const build = async ({
if (pipfileLockDir) {
debug('Found "Pipfile.lock"');
try {
const json = await readFile(join(pipfileLockDir, 'Pipfile.lock'), 'utf8');
const obj = JSON.parse(json);
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({
code: 'INVALID_PIPFILE_LOCK',
message: 'Unable to parse Pipfile.lock',
});
}
// Convert Pipenv.Lock to requirements.txt.
// We use a different`workPath` here because we want `pipfile-requirements` and it's dependencies
// to not be part of the lambda environment. By using pip's `--target` directive we can isolate
// it into a separate folder.
const tempDir = await getWriteableDirectory();
await installRequirement({
pythonPath,
pipPath,
dependency: 'pipfile-requirements',
version: '0.3.0',
workPath: tempDir,
@@ -130,6 +169,8 @@ export const build = async ({
debug('Found local "requirements.txt"');
const requirementsTxtPath = fsFiles[requirementsTxt].fsPath;
await installRequirementsFile({
pythonPath,
pipPath,
filePath: requirementsTxtPath,
workPath,
meta,
@@ -138,6 +179,8 @@ export const build = async ({
debug('Found global "requirements.txt"');
const requirementsTxtPath = fsFiles['requirements.txt'].fsPath;
await installRequirementsFile({
pythonPath,
pipPath,
filePath: requirementsTxtPath,
workPath,
meta,
@@ -163,7 +206,7 @@ export const build = async ({
await writeFile(join(workPath, `${handlerPyFilename}.py`), handlerPyContents);
// Use the system-installed version of `python3` when running via `vercel dev`
const runtime = meta.isDev ? 'python3' : 'python3.6';
const runtime = meta.isDev ? 'python3' : pythonRuntime;
const globOptions: GlobOptions = {
cwd: workPath,

View File

@@ -1,6 +1,5 @@
import execa from 'execa';
import { Meta, debug } from '@vercel/build-utils';
const pipPath = 'pip3';
const makeDependencyCheckCode = (dependency: string) => `
from importlib import util
@@ -9,10 +8,14 @@ spec = util.find_spec(dep)
print(spec.origin)
`;
async function isInstalled(dependency: string, cwd: string) {
async function isInstalled(
pythonPath: string,
dependency: string,
cwd: string
) {
try {
const { stdout } = await execa(
'python3',
pythonPath,
['-c', makeDependencyCheckCode(dependency)],
{
stdio: 'pipe',
@@ -33,10 +36,14 @@ dependencies = distutils.text_file.TextFile(filename='${requirementsPath}').read
pkg_resources.require(dependencies)
`;
async function areRequirementsInstalled(requirementsPath: string, cwd: string) {
async function areRequirementsInstalled(
pythonPath: string,
requirementsPath: string,
cwd: string
) {
try {
await execa(
'python3',
pythonPath,
['-c', makeRequirementsCheckCode(requirementsPath)],
{
stdio: 'pipe',
@@ -49,7 +56,7 @@ async function areRequirementsInstalled(requirementsPath: string, cwd: string) {
}
}
async function pipInstall(workPath: string, args: string[]) {
async function pipInstall(pipPath: string, workPath: string, args: string[]) {
const target = '.';
// See: https://github.com/pypa/pip/issues/4222#issuecomment-417646535
//
@@ -66,19 +73,21 @@ async function pipInstall(workPath: string, args: string[]) {
target,
...args,
];
debug(`Running "pip3 ${cmdArgs.join(' ')}"...`);
const pretty = `${pipPath} ${cmdArgs.join(' ')}`;
debug(`Running "${pretty}"...`);
try {
await execa(pipPath, cmdArgs, {
cwd: workPath,
stdio: 'pipe',
});
} catch (err) {
console.log(`Failed to run "pip3 ${cmdArgs.join(' ')}"`);
console.log(`Failed to run "${pretty}"`);
throw err;
}
}
interface InstallRequirementArg {
pythonPath: string;
pipPath: string;
dependency: string;
version: string;
workPath: string;
@@ -91,23 +100,27 @@ interface InstallRequirementArg {
// so vc_init should do runtime version checks to be compatible with any recent
// version of its dependencies
export async function installRequirement({
pythonPath,
pipPath,
dependency,
version,
workPath,
meta,
args = [],
}: InstallRequirementArg) {
if (meta.isDev && (await isInstalled(dependency, workPath))) {
if (meta.isDev && (await isInstalled(pythonPath, dependency, workPath))) {
debug(
`Skipping ${dependency} dependency installation, already installed in ${workPath}`
);
return;
}
const exact = `${dependency}==${version}`;
await pipInstall(workPath, [exact, ...args]);
await pipInstall(pipPath, workPath, [exact, ...args]);
}
interface InstallRequirementsFileArg {
pythonPath: string;
pipPath: string;
filePath: string;
workPath: string;
meta: Meta;
@@ -115,14 +128,19 @@ interface InstallRequirementsFileArg {
}
export async function installRequirementsFile({
pythonPath,
pipPath,
filePath,
workPath,
meta,
args = [],
}: InstallRequirementsFileArg) {
if (meta.isDev && (await areRequirementsInstalled(filePath, workPath))) {
if (
meta.isDev &&
(await areRequirementsInstalled(pythonPath, filePath, workPath))
) {
debug(`Skipping requirements file installation, already installed`);
return;
}
await pipInstall(workPath, ['--upgrade', '-r', filePath, ...args]);
await pipInstall(pipPath, workPath, ['--upgrade', '-r', filePath, ...args]);
}

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

@@ -13,6 +13,6 @@
"outDir": "dist",
"types": ["node"],
"strict": true,
"target": "esnext"
"target": "es2018"
}
}

View File

@@ -20,7 +20,12 @@ async function nowDeploy(bodies, randomness, uploadNowJson) {
(path.extname(n) === '.sh' ? 0o100755 : 0o100644),
}));
const { FORCE_BUILD_IN_REGION, NOW_DEBUG, VERCEL_DEBUG } = process.env;
const {
FORCE_BUILD_IN_REGION,
NOW_DEBUG,
VERCEL_DEBUG,
VERCEL_BUILDER_DEBUG,
} = process.env;
const nowJson = JSON.parse(bodies['vercel.json'] || bodies['now.json']);
const nowDeployPayload = {
@@ -34,6 +39,7 @@ async function nowDeploy(bodies, randomness, uploadNowJson) {
FORCE_BUILD_IN_REGION,
NOW_DEBUG,
VERCEL_DEBUG,
VERCEL_BUILDER_DEBUG,
},
},
name: 'test2020',

View File

@@ -2537,7 +2537,7 @@
dependencies:
"@types/yargs-parser" "*"
"@types/yazl@^2.4.1":
"@types/yazl@2.4.2", "@types/yazl@^2.4.1":
version "2.4.2"
resolved "https://registry.yarnpkg.com/@types/yazl/-/yazl-2.4.2.tgz#d5f8a4752261badbf1a36e8b49e042dc18ec84bc"
integrity sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==
@@ -2613,10 +2613,10 @@
"@typescript-eslint/types" "4.28.0"
eslint-visitor-keys "^2.0.0"
"@vercel/fun@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@vercel/fun/-/fun-1.0.1.tgz#4b25f50c27bbbdca5b7e71f25ef5018701467962"
integrity sha512-v++KDTdKIr0357sm0AbyYk5jLmsqfUlOKhTsaN0Z/XHpk2YJHOLlGFTDzZPt+8i+ofdPG8lCfht729DHNQskJg==
"@vercel/fun@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@vercel/fun/-/fun-1.0.3.tgz#5e5c4921a3a3a35ee129ce063e6b3f5c4b1eae48"
integrity sha512-9LKNosC+Sdx7ZNU7dnj7XXasHzgRkuDnz9+N6JzSVvb0v8KOZN0ky4+wn3c1B0/wF3dTn2W5nEzgmnsrVCDtkQ==
dependencies:
"@tootallnate/once" "2.0.0"
async-listen "1.0.0"
@@ -2629,6 +2629,7 @@
node-fetch "2.6.1"
path-match "1.2.4"
promisepipe "3.0.0"
semver "7.3.5"
stat-mode "0.3.0"
stream-to-promise "2.2.0"
tar "4.4.18"
@@ -8992,10 +8993,10 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1"
node-fetch@2, node-fetch@^2:
version "2.6.5"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
node-fetch@2:
version "2.6.6"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
dependencies:
whatwg-url "^5.0.0"
@@ -9009,6 +9010,13 @@ node-fetch@2.6.1, node-fetch@^2.2.1, node-fetch@^2.3.0, node-fetch@^2.5.0, node-
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@^2:
version "2.6.5"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
dependencies:
whatwg-url "^5.0.0"
node-gyp-build@^4.2.2:
version "4.2.3"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739"
@@ -10665,7 +10673,7 @@ semver@6.1.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==
semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
semver@7.3.5, semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@@ -12618,6 +12626,13 @@ yazl@2.4.3:
dependencies:
buffer-crc32 "~0.2.3"
yazl@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35"
integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==
dependencies:
buffer-crc32 "~0.2.3"
yn@3.1.1, yn@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"