mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
118 Commits
vercel-plu
...
vercel-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01ea7b4b2a | ||
|
|
475a227ba9 | ||
|
|
40ca92f2e9 | ||
|
|
465129e62e | ||
|
|
bf0d5a7f29 | ||
|
|
d3ef240f6e | ||
|
|
5b26ebc7b8 | ||
|
|
3427ad6ce0 | ||
|
|
4ab5e4326b | ||
|
|
d24a3ce3ab | ||
|
|
29a44db8d9 | ||
|
|
695f3a9212 | ||
|
|
3ff777b8ed | ||
|
|
d94b9806ab | ||
|
|
35c8fc2729 | ||
|
|
0a468fd6d7 | ||
|
|
d31ebbabe4 | ||
|
|
09c9b71adb | ||
|
|
5975db4d66 | ||
|
|
2c86ac654c | ||
|
|
ca5f066eb9 | ||
|
|
410ef86102 | ||
|
|
6792edf32a | ||
|
|
67de167a7e | ||
|
|
0c5c05d90b | ||
|
|
fe43c9c4b2 | ||
|
|
d6a5aa4f6d | ||
|
|
1c3701628d | ||
|
|
45689f22ab | ||
|
|
2c3ddffaac | ||
|
|
c3ea0195c2 | ||
|
|
5f5e50cff0 | ||
|
|
160f4d46d9 | ||
|
|
8d619bd7cc | ||
|
|
b94337d842 | ||
|
|
34f4222ca2 | ||
|
|
5de045edd7 | ||
|
|
5efd3b98de | ||
|
|
82c83312c7 | ||
|
|
5ccb983007 | ||
|
|
7a921399be | ||
|
|
3900f2f982 | ||
|
|
09939f1e07 | ||
|
|
fc3a3ca81f | ||
|
|
ba7bf2e4a6 | ||
|
|
00641037fc | ||
|
|
6f4a1b527b | ||
|
|
1b95576dd2 | ||
|
|
9227471aca | ||
|
|
bf060296eb | ||
|
|
9b3aa41f2e | ||
|
|
ae36585cdb | ||
|
|
e4c636ddd2 | ||
|
|
ae3b25be4b | ||
|
|
a64ed13a40 | ||
|
|
6c1c0e6676 | ||
|
|
82fdd5d121 | ||
|
|
8b40f4435e | ||
|
|
38c87602bb | ||
|
|
7aef3013e7 | ||
|
|
c18676ab4d | ||
|
|
df450c815d | ||
|
|
792ab38760 | ||
|
|
0bba3e76c1 | ||
|
|
3d961ffbb9 | ||
|
|
a3039f57bb | ||
|
|
5499fa9a04 | ||
|
|
b9fd64faff | ||
|
|
1202ff7b2b | ||
|
|
abd9f019f1 | ||
|
|
edb5eead81 | ||
|
|
6b865ff753 | ||
|
|
4fd0734c48 | ||
|
|
f815421acb | ||
|
|
5da926fee1 | ||
|
|
3559531e4c | ||
|
|
449a3b3648 | ||
|
|
7bd338618c | ||
|
|
9048a6f584 | ||
|
|
0cacb1bdac | ||
|
|
318bf35f82 | ||
|
|
cc7b2691c1 | ||
|
|
3a75c37adc | ||
|
|
acd0e84e73 | ||
|
|
49087d5aac | ||
|
|
7d7f3df980 | ||
|
|
5cf0c316e9 | ||
|
|
f4501433c8 | ||
|
|
19831593ce | ||
|
|
5d85bb1426 | ||
|
|
f194d54b0c | ||
|
|
6542086843 | ||
|
|
2721b3449d | ||
|
|
adb284519a | ||
|
|
b2d91f3121 | ||
|
|
32664cd13b | ||
|
|
db468c489a | ||
|
|
edd9bb506c | ||
|
|
a72549a290 | ||
|
|
4aa6a13912 | ||
|
|
81ea0082f1 | ||
|
|
6dff0875f5 | ||
|
|
30aa392c0a | ||
|
|
c4fc060030 | ||
|
|
3fa08bf64f | ||
|
|
43056bde1f | ||
|
|
a49966b9b4 | ||
|
|
7f55de71bb | ||
|
|
db8e36e04c | ||
|
|
82924bb5c4 | ||
|
|
18b5fac93e | ||
|
|
a6012e600b | ||
|
|
c3abf73f58 | ||
|
|
4873b8b379 | ||
|
|
6248139281 | ||
|
|
507a5de3cd | ||
|
|
be1c78e72f | ||
|
|
c277c649c6 |
40
.github/CODEOWNERS
vendored
40
.github/CODEOWNERS
vendored
@@ -4,24 +4,26 @@
|
||||
* @TooTallNate
|
||||
/.github/workflows @AndyBitz @styfle
|
||||
/packages/frameworks @AndyBitz
|
||||
/packages/cli/src/commands/dev @TooTallNate @styfle @AndyBitz
|
||||
/packages/cli/src/util/dev @TooTallNate @styfle @AndyBitz
|
||||
/packages/cli/src/commands/domains @javivelasco @mglagola @anatrajkovska
|
||||
/packages/cli/src/commands/certs @javivelasco @mglagola @anatrajkovska
|
||||
/packages/cli/src/commands/env @styfle @lucleray
|
||||
/packages/client @rdev @styfle @TooTallNate
|
||||
/packages/build-utils @styfle @AndyBitz @TooTallNate
|
||||
/packages/node @styfle @TooTallNate @lucleray
|
||||
/packages/node-bridge @styfle @TooTallNate @lucleray
|
||||
/packages/next @Timer @ijjk
|
||||
/packages/go @styfle @TooTallNate
|
||||
/packages/python @styfle @TooTallNate
|
||||
/packages/ruby @styfle @coetry @TooTallNate
|
||||
/packages/static-build @styfle @AndyBitz
|
||||
/packages/routing-utils @styfle @dav-is @ijjk
|
||||
/examples @mcsdevv @timothyis
|
||||
/packages/cli/src/commands/build @TooTallNate @styfle @AndyBitz @gdborton @jaredpalmer
|
||||
/packages/cli/src/commands/dev @TooTallNate @styfle @AndyBitz
|
||||
/packages/cli/src/util/dev @TooTallNate @styfle @AndyBitz
|
||||
/packages/cli/src/commands/domains @javivelasco @mglagola @anatrajkovska
|
||||
/packages/cli/src/commands/certs @javivelasco @mglagola @anatrajkovska
|
||||
/packages/cli/src/commands/env @styfle @lucleray
|
||||
/packages/client @styfle @TooTallNate
|
||||
/packages/build-utils @styfle @AndyBitz @TooTallNate
|
||||
/packages/middleware @gdborton @javivelasco
|
||||
/packages/node @styfle @TooTallNate @lucleray
|
||||
/packages/node-bridge @styfle @TooTallNate @lucleray
|
||||
/packages/next @Timer @ijjk
|
||||
/packages/go @styfle @TooTallNate
|
||||
/packages/python @styfle @TooTallNate
|
||||
/packages/ruby @styfle @TooTallNate
|
||||
/packages/static-build @styfle @AndyBitz
|
||||
/packages/routing-utils @styfle @dav-is @ijjk
|
||||
/examples @mcsdevv
|
||||
/examples/create-react-app @Timer
|
||||
/examples/nextjs @timneutkens @Timer
|
||||
/examples/hugo @mcsdevv @timothyis @styfle
|
||||
/examples/jekyll @mcsdevv @timothyis @styfle
|
||||
/examples/zola @mcsdevv @timothyis @styfle
|
||||
/examples/hugo @mcsdevv @styfle
|
||||
/examples/jekyll @mcsdevv @styfle
|
||||
/examples/zola @mcsdevv @styfle
|
||||
|
||||
2
.github/workflows/cancel.yml
vendored
2
.github/workflows/cancel.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/test-integration-dev.yml
vendored
2
.github/workflows/test-integration-dev.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Dev
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 75
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,3 +27,4 @@ test/lib/deployment/failed-page.txt
|
||||
/public
|
||||
__pycache__
|
||||
.vercel
|
||||
.output
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -14,8 +14,6 @@ const frameworks = (_frameworks as Framework[])
|
||||
sort: undefined,
|
||||
dependency: undefined,
|
||||
defaultRoutes: undefined,
|
||||
devCommand: undefined,
|
||||
buildCommand: undefined,
|
||||
};
|
||||
|
||||
if (framework.logo) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "API for the vercel/vercel repo",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"vercel-build": "yarn --cwd .. && node ../utils/run.js build all"
|
||||
"vercel-build": "node ../utils/run.js build all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "5.11.1",
|
||||
|
||||
9
errors/no-single-file-deployments.md
Normal file
9
errors/no-single-file-deployments.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# No Single File Deployments
|
||||
|
||||
#### Why This Error Occurred
|
||||
|
||||
You attempted to create a Vercel deployment where the input is a file, rather than a directory. Previously this was allowed, however this behavior has been removed as of Vercel CLI v24.0.0 because it exposed a potential security risk if the user accidentally created a deployment from a sensitive file.
|
||||
|
||||
#### Possible Ways to Fix It
|
||||
|
||||
- Run the `vercel deploy` command against a directory, instead of a file.
|
||||
15787
examples/nextjs/package-lock.json
generated
Normal file
15787
examples/nextjs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
8
examples/remix/.gitignore
vendored
Normal file
8
examples/remix/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
|
||||
.cache
|
||||
.vercel
|
||||
.output
|
||||
|
||||
public/build
|
||||
api/_build
|
||||
34
examples/remix/README.md
Normal file
34
examples/remix/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Welcome to Remix!
|
||||
|
||||
- [Remix Docs](https://remix.run/docs)
|
||||
|
||||
## Deployment
|
||||
|
||||
After having run the `create-remix` command and selected "Vercel" as a deployment target, you only need to [import your Git repository](https://vercel.com/new) into Vercel, and it will be deployed.
|
||||
|
||||
If you'd like to avoid using a Git repository, you can also deploy the directory by running [Vercel CLI](https://vercel.com/cli):
|
||||
|
||||
```sh
|
||||
npm i -g vercel
|
||||
vercel
|
||||
```
|
||||
|
||||
It is generally recommended to use a Git repository, because future commits will then automatically be deployed by Vercel, through its [Git Integration](https://vercel.com/docs/concepts/git).
|
||||
|
||||
## Development
|
||||
|
||||
To run your Remix app locally, make sure your project's local dependencies are installed:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
Afterwards, start the Remix development server like so:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go!
|
||||
|
||||
If you're used to using the `vercel dev` command provided by [Vercel CLI](https://vercel.com/cli) instead, you can also use that, but it's not needed.
|
||||
5
examples/remix/api/index.js
Normal file
5
examples/remix/api/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { createRequestHandler } = require("@remix-run/vercel");
|
||||
|
||||
module.exports = createRequestHandler({
|
||||
build: require("./_build")
|
||||
});
|
||||
4
examples/remix/app/entry.client.tsx
Normal file
4
examples/remix/app/entry.client.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import { hydrate } from "react-dom";
|
||||
import { RemixBrowser } from "remix";
|
||||
|
||||
hydrate(<RemixBrowser />, document);
|
||||
21
examples/remix/app/entry.server.tsx
Normal file
21
examples/remix/app/entry.server.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { RemixServer } from "remix";
|
||||
import type { EntryContext } from "remix";
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
let markup = renderToString(
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
return new Response("<!DOCTYPE html>" + markup, {
|
||||
status: responseStatusCode,
|
||||
headers: responseHeaders
|
||||
});
|
||||
}
|
||||
178
examples/remix/app/root.tsx
Normal file
178
examples/remix/app/root.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
Link,
|
||||
Links,
|
||||
LiveReload,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useCatch
|
||||
} from "remix";
|
||||
import type { LinksFunction } from "remix";
|
||||
|
||||
import globalStylesUrl from "~/styles/global.css";
|
||||
import darkStylesUrl from "~/styles/dark.css";
|
||||
|
||||
// https://remix.run/api/app#links
|
||||
export let links: LinksFunction = () => {
|
||||
return [
|
||||
{ rel: "stylesheet", href: globalStylesUrl },
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: darkStylesUrl,
|
||||
media: "(prefers-color-scheme: dark)"
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// https://remix.run/api/conventions#default-export
|
||||
// https://remix.run/api/conventions#route-filenames
|
||||
export default function App() {
|
||||
return (
|
||||
<Document>
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
// https://remix.run/docs/en/v1/api/conventions#errorboundary
|
||||
export function ErrorBoundary({ error }: { error: Error }) {
|
||||
console.error(error);
|
||||
return (
|
||||
<Document title="Error!">
|
||||
<Layout>
|
||||
<div>
|
||||
<h1>There was an error</h1>
|
||||
<p>{error.message}</p>
|
||||
<hr />
|
||||
<p>
|
||||
Hey, developer, you should replace this with what you want your
|
||||
users to see.
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
// https://remix.run/docs/en/v1/api/conventions#catchboundary
|
||||
export function CatchBoundary() {
|
||||
let caught = useCatch();
|
||||
|
||||
let message;
|
||||
switch (caught.status) {
|
||||
case 401:
|
||||
message = (
|
||||
<p>
|
||||
Oops! Looks like you tried to visit a page that you do not have access
|
||||
to.
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case 404:
|
||||
message = (
|
||||
<p>Oops! Looks like you tried to visit a page that does not exist.</p>
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(caught.data || caught.statusText);
|
||||
}
|
||||
|
||||
return (
|
||||
<Document title={`${caught.status} ${caught.statusText}`}>
|
||||
<Layout>
|
||||
<h1>
|
||||
{caught.status}: {caught.statusText}
|
||||
</h1>
|
||||
{message}
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
function Document({
|
||||
children,
|
||||
title
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
{title ? <title>{title}</title> : null}
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
{process.env.NODE_ENV === "development" && <LiveReload />}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="remix-app">
|
||||
<header className="remix-app__header">
|
||||
<div className="container remix-app__header-content">
|
||||
<Link to="/" title="Remix" className="remix-app__header-home-link">
|
||||
<RemixLogo />
|
||||
</Link>
|
||||
<nav aria-label="Main navigation" className="remix-app__header-nav">
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/">Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://remix.run/docs">Remix Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/remix-run/remix">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div className="remix-app__main">
|
||||
<div className="container remix-app__main-content">{children}</div>
|
||||
</div>
|
||||
<footer className="remix-app__footer">
|
||||
<div className="container remix-app__footer-content">
|
||||
<p>© You!</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RemixLogo() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 659 165"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
aria-labelledby="remix-run-logo-title"
|
||||
role="img"
|
||||
width="106"
|
||||
height="30"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title id="remix-run-logo-title">Remix Logo</title>
|
||||
<path d="M0 161V136H45.5416C53.1486 136 54.8003 141.638 54.8003 145V161H0Z M133.85 124.16C135.3 142.762 135.3 151.482 135.3 161H92.2283C92.2283 158.927 92.2653 157.03 92.3028 155.107C92.4195 149.128 92.5411 142.894 91.5717 130.304C90.2905 111.872 82.3473 107.776 67.7419 107.776H54.8021H0V74.24H69.7918C88.2407 74.24 97.4651 68.632 97.4651 53.784C97.4651 40.728 88.2407 32.816 69.7918 32.816H0V0H77.4788C119.245 0 140 19.712 140 51.2C140 74.752 125.395 90.112 105.665 92.672C122.32 96 132.057 105.472 133.85 124.16Z" />
|
||||
<path d="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" />
|
||||
<path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" />
|
||||
<path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" />
|
||||
<path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
44
examples/remix/app/routes/demos/about.tsx
Normal file
44
examples/remix/app/routes/demos/about.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
examples/remix/app/routes/demos/about/index.tsx
Normal file
17
examples/remix/app/routes/demos/about/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
20
examples/remix/app/routes/demos/about/whoa.tsx
Normal file
20
examples/remix/app/routes/demos/about/whoa.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
101
examples/remix/app/routes/demos/actions.tsx
Normal file
101
examples/remix/app/routes/demos/actions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
3
examples/remix/app/routes/demos/correct.tsx
Normal file
3
examples/remix/app/routes/demos/correct.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function NiceWork() {
|
||||
return <h1>You got it right!</h1>;
|
||||
}
|
||||
43
examples/remix/app/routes/demos/params.tsx
Normal file
43
examples/remix/app/routes/demos/params.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
110
examples/remix/app/routes/demos/params/$id.tsx
Normal file
110
examples/remix/app/routes/demos/params/$id.tsx
Normal 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...",
|
||||
};
|
||||
};
|
||||
40
examples/remix/app/routes/demos/params/index.tsx
Normal file
40
examples/remix/app/routes/demos/params/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
100
examples/remix/app/routes/index.tsx
Normal file
100
examples/remix/app/routes/index.tsx
Normal 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 you’re 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>
|
||||
);
|
||||
}
|
||||
7
examples/remix/app/styles/dark.css
Normal file
7
examples/remix/app/styles/dark.css
Normal 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%);
|
||||
}
|
||||
26
examples/remix/app/styles/demos/about.css
Normal file
26
examples/remix/app/styles/demos/about.css
Normal 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;
|
||||
}
|
||||
216
examples/remix/app/styles/global.css
Normal file
216
examples/remix/app/styles/global.css
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* You can just delete everything here or keep whatever you like, it's just a
|
||||
* quick baseline!
|
||||
*/
|
||||
:root {
|
||||
--color-foreground: hsl(0, 0%, 7%);
|
||||
--color-background: hsl(0, 0%, 100%);
|
||||
--color-links: hsl(213, 100%, 52%);
|
||||
--color-links-hover: hsl(213, 100%, 43%);
|
||||
--color-border: hsl(0, 0%, 82%);
|
||||
--font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica, Roboto,
|
||||
Arial, sans-serif, system-ui, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: var(--color-links) solid 2px;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-links);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-links-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background-color: var(--color-border);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
input:where([type="text"]),
|
||||
input:where([type="search"]) {
|
||||
display: block;
|
||||
border: 1px solid var(--color-border);
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
height: calc(1ch + 1.5em);
|
||||
padding-right: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
background-color: hsl(0 0% 100% / 20%);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
--gutter: 16px;
|
||||
width: 1024px;
|
||||
max-width: calc(100% - var(--gutter) * 2);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.remix-app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: calc(100vh - env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.remix-app > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remix-app__header {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.remix-app__header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remix-app__header-home-link {
|
||||
width: 106px;
|
||||
height: 30px;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.remix-app__header-nav ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
.remix-app__header-nav li {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remix-app__main {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.remix-app__footer {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.remix-app__footer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remix__page {
|
||||
--gap: 1rem;
|
||||
--space: 2rem;
|
||||
display: grid;
|
||||
grid-auto-rows: min-content;
|
||||
gap: var(--gap);
|
||||
padding-top: var(--space);
|
||||
padding-bottom: var(--space);
|
||||
}
|
||||
|
||||
@media print, screen and (min-width: 640px) {
|
||||
.remix__page {
|
||||
--gap: 2rem;
|
||||
grid-auto-rows: unset;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.remix__page {
|
||||
--gap: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.remix__page > main > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.remix__page > main > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.remix__page > aside {
|
||||
margin: 0;
|
||||
padding: 1.5ch 2ch;
|
||||
border: solid 1px var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.remix__page > aside > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.remix__page > aside > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.remix__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.remix__form > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
8345
examples/remix/package-lock.json
generated
Normal file
8345
examples/remix/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
examples/remix/package.json
Normal file
29
examples/remix/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "remix-app-template",
|
||||
"description": "",
|
||||
"license": "",
|
||||
"scripts": {
|
||||
"build": "remix build",
|
||||
"dev": "remix dev",
|
||||
"postinstall": "remix setup node"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/react": "^1.0.6",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"remix": "^1.0.6",
|
||||
"@remix-run/serve": "^1.0.6",
|
||||
"@remix-run/vercel": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.0.6",
|
||||
"@types/react": "^17.0.24",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
BIN
examples/remix/public/favicon.ico
Normal file
BIN
examples/remix/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
9
examples/remix/remix.config.js
Normal file
9
examples/remix/remix.config.js
Normal 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
2
examples/remix/remix.env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="@remix-run/dev" />
|
||||
/// <reference types="@remix-run/node/globals" />
|
||||
18
examples/remix/tsconfig.json
Normal file
18
examples/remix/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
7
examples/remix/vercel.json
Normal file
7
examples/remix/vercel.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"build": {
|
||||
"env": {
|
||||
"ENABLE_FILE_SYSTEM_API": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.12.3-canary.17",
|
||||
"version": "2.12.3-canary.43",
|
||||
"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.17",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"aggregate-error": "3.0.1",
|
||||
"async-retry": "1.2.3",
|
||||
|
||||
@@ -1,128 +1,300 @@
|
||||
import fs from 'fs-extra';
|
||||
import { join, dirname, relative } from 'path';
|
||||
import { join, parse, relative, dirname, basename, extname } from 'path';
|
||||
import glob from './fs/glob';
|
||||
import { normalizePath } from './fs/normalize-path';
|
||||
import { FILES_SYMBOL, Lambda } from './lambda';
|
||||
import type FileBlob from './file-blob';
|
||||
import type { BuilderFunctions, BuildOptions, Files } from './types';
|
||||
import minimatch from 'minimatch';
|
||||
import type { BuildOptions, Files } from './types';
|
||||
import { debug, getIgnoreFilter } from '.';
|
||||
|
||||
// `.output` was already created by the Build Command, so we have
|
||||
// to ensure its contents don't get bundled into the Lambda. Similarily,
|
||||
// we don't want to bundle anything from `.vercel` either. Lastly,
|
||||
// Builders/Runtimes didn't have `vercel.json` or `now.json`.
|
||||
const ignoredPaths = ['.output', '.vercel', 'vercel.json', 'now.json'];
|
||||
|
||||
const shouldIgnorePath = (
|
||||
file: string,
|
||||
ignoreFilter: any,
|
||||
ignoreFile: boolean
|
||||
) => {
|
||||
const isNative = ignoredPaths.some(item => {
|
||||
return file.startsWith(item);
|
||||
});
|
||||
|
||||
if (!ignoreFile) {
|
||||
return isNative;
|
||||
}
|
||||
|
||||
return isNative || ignoreFilter(file);
|
||||
};
|
||||
|
||||
const getSourceFiles = async (workPath: string, ignoreFilter: any) => {
|
||||
const list = await glob('**', {
|
||||
cwd: workPath,
|
||||
});
|
||||
|
||||
// We're not passing this as an `ignore` filter to the `glob` function above,
|
||||
// so that we can re-use exactly the same `getIgnoreFilter` method that the
|
||||
// Build Step uses (literally the same code). Note that this exclusion only applies
|
||||
// when deploying. Locally, another exclusion is needed, which is handled
|
||||
// further below in the `convertRuntimeToPlugin` function.
|
||||
for (const file in list) {
|
||||
if (shouldIgnorePath(file, ignoreFilter, true)) {
|
||||
delete list[file];
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert legacy Runtime to a Plugin.
|
||||
* @param buildRuntime - a legacy build() function from a Runtime
|
||||
* @param packageName - the name of the package, for example `vercel-plugin-python`
|
||||
* @param ext - the file extension, for example `.py`
|
||||
*/
|
||||
export function convertRuntimeToPlugin(
|
||||
buildRuntime: (options: BuildOptions) => Promise<{ output: Lambda }>,
|
||||
packageName: string,
|
||||
ext: string
|
||||
) {
|
||||
// This `build()` signature should match `plugin.build()` signature in `vercel build`.
|
||||
return async function build({ workPath }: { workPath: string }) {
|
||||
const opts = { cwd: workPath };
|
||||
const files = await glob('**', opts);
|
||||
delete files['vercel.json']; // Builders/Runtimes didn't have vercel.json
|
||||
const entrypoints = await glob(`api/**/*${ext}`, opts);
|
||||
const functionsManifest: { [key: string]: any } = {};
|
||||
const functions = await readVercelConfigFunctions(workPath);
|
||||
const traceDir = join(workPath, '.output', 'runtime-traced-files');
|
||||
// We also don't want to provide any files to Runtimes that were ignored
|
||||
// through `.vercelignore` or `.nowignore`, because the Build Step does the same.
|
||||
const ignoreFilter = await getIgnoreFilter(workPath);
|
||||
|
||||
// Retrieve the files that are currently available on the File System,
|
||||
// before the Legacy Runtime has even started to build.
|
||||
const sourceFilesPreBuild = await getSourceFiles(workPath, ignoreFilter);
|
||||
|
||||
// Instead of doing another `glob` to get all the matching source files,
|
||||
// we'll filter the list of existing files down to only the ones
|
||||
// that are matching the entrypoint pattern, so we're first creating
|
||||
// a clean new list to begin.
|
||||
const entrypoints = Object.assign({}, sourceFilesPreBuild);
|
||||
|
||||
const entrypointMatch = new RegExp(`^api/.*${ext}$`);
|
||||
|
||||
// Up next, we'll strip out the files from the list of entrypoints
|
||||
// that aren't actually considered entrypoints.
|
||||
for (const file in entrypoints) {
|
||||
if (!entrypointMatch.test(file)) {
|
||||
delete entrypoints[file];
|
||||
}
|
||||
}
|
||||
|
||||
const pages: { [key: string]: any } = {};
|
||||
const pluginName = packageName.replace('vercel-plugin-', '');
|
||||
const outputPath = join(workPath, '.output');
|
||||
|
||||
const traceDir = join(
|
||||
outputPath,
|
||||
`inputs`,
|
||||
// Legacy Runtimes can only provide API Routes, so that's
|
||||
// why we can use this prefix for all of them. Here, we have to
|
||||
// make sure to not use a cryptic hash name, because people
|
||||
// need to be able to easily inspect the output.
|
||||
`api-routes-${pluginName}`
|
||||
);
|
||||
|
||||
await fs.ensureDir(traceDir);
|
||||
|
||||
for (const entrypoint of Object.keys(entrypoints)) {
|
||||
const key =
|
||||
Object.keys(functions).find(
|
||||
src => src === entrypoint || minimatch(entrypoint, src)
|
||||
) || '';
|
||||
const config = functions[key] || {};
|
||||
const entryRoot = join(outputPath, 'server', 'pages');
|
||||
|
||||
for (const entrypoint of Object.keys(entrypoints)) {
|
||||
const { output } = await buildRuntime({
|
||||
files,
|
||||
files: sourceFilesPreBuild,
|
||||
entrypoint,
|
||||
workPath,
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
includeFiles: config.includeFiles,
|
||||
excludeFiles: config.excludeFiles,
|
||||
},
|
||||
meta: {
|
||||
avoidTopLevelInstall: true,
|
||||
skipDownload: true,
|
||||
},
|
||||
});
|
||||
|
||||
functionsManifest[entrypoint] = {
|
||||
handler: output.handler,
|
||||
runtime: output.runtime,
|
||||
memory: config.memory || output.memory,
|
||||
maxDuration: config.maxDuration || output.maxDuration,
|
||||
environment: output.environment,
|
||||
allowQuery: output.allowQuery,
|
||||
regions: output.regions,
|
||||
};
|
||||
|
||||
// @ts-ignore This symbol is a private API
|
||||
const lambdaFiles: Files = output[FILES_SYMBOL];
|
||||
|
||||
const entry = join(workPath, '.output', 'server', 'pages', entrypoint);
|
||||
await fs.ensureDir(dirname(entry));
|
||||
await linkOrCopy(files[entrypoint].fsPath, entry);
|
||||
|
||||
const tracedFiles: {
|
||||
absolutePath: string;
|
||||
relativePath: string;
|
||||
}[] = [];
|
||||
|
||||
Object.entries(lambdaFiles).forEach(async ([relPath, file]) => {
|
||||
const newPath = join(traceDir, relPath);
|
||||
tracedFiles.push({ absolutePath: newPath, relativePath: relPath });
|
||||
if (file.fsPath) {
|
||||
await linkOrCopy(file.fsPath, newPath);
|
||||
} else if (file.type === 'FileBlob') {
|
||||
const { data, mode } = file as FileBlob;
|
||||
await fs.writeFile(newPath, data, { mode });
|
||||
} else {
|
||||
throw new Error(`Unknown file type: ${file.type}`);
|
||||
// When deploying, the `files` that are passed to the Legacy Runtimes already
|
||||
// have certain files that are ignored stripped, but locally, that list of
|
||||
// files isn't used by the Legacy Runtimes, so we need to apply the filters
|
||||
// to the outputs that they are returning instead.
|
||||
for (const file in lambdaFiles) {
|
||||
if (shouldIgnorePath(file, ignoreFilter, false)) {
|
||||
delete lambdaFiles[file];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let handlerFileBase = output.handler;
|
||||
let handlerFile = lambdaFiles[handlerFileBase];
|
||||
let handlerHasImport = false;
|
||||
|
||||
const { handler } = output;
|
||||
const handlerMethod = handler.split('.').pop();
|
||||
const handlerFileName = handler.replace(`.${handlerMethod}`, '');
|
||||
|
||||
// For compiled languages, the launcher file for the Lambda generated
|
||||
// by the Legacy Runtime matches the `handler` defined for it, but for
|
||||
// interpreted languages, the `handler` consists of the launcher file name
|
||||
// without an extension, plus the name of the method inside of that file
|
||||
// that should be invoked, so we have to construct the file path explicitly.
|
||||
if (!handlerFile) {
|
||||
handlerFileBase = handlerFileName + ext;
|
||||
handlerFile = lambdaFiles[handlerFileBase];
|
||||
handlerHasImport = true;
|
||||
}
|
||||
|
||||
if (!handlerFile || !handlerFile.fsPath) {
|
||||
throw new Error(
|
||||
`Could not find a handler file. Please ensure that \`files\` for the returned \`Lambda\` contains an \`FileFsRef\` named "${handlerFileBase}" with a valid \`fsPath\`.`
|
||||
);
|
||||
}
|
||||
|
||||
const handlerExtName = extname(handlerFile.fsPath);
|
||||
|
||||
const entryBase = basename(entrypoint).replace(ext, handlerExtName);
|
||||
const entryPath = join(dirname(entrypoint), entryBase);
|
||||
const entry = join(entryRoot, entryPath);
|
||||
|
||||
// Create the parent directory of the API Route that will be created
|
||||
// for the current entrypoint inside of `.output/server/pages/api`.
|
||||
await fs.ensureDir(dirname(entry));
|
||||
|
||||
// For compiled languages, the launcher file will be binary and therefore
|
||||
// won't try to import a user-provided request handler (instead, it will
|
||||
// contain it). But for interpreted languages, the launcher might try to
|
||||
// load a user-provided request handler from the source file instead of bundling
|
||||
// it, so we have to adjust the import statement inside the launcher to point
|
||||
// to the respective source file. Previously, Legacy Runtimes simply expected
|
||||
// the user-provided request-handler to be copied right next to the launcher,
|
||||
// but with the new File System API, files won't be moved around unnecessarily.
|
||||
if (handlerHasImport) {
|
||||
const { fsPath } = handlerFile;
|
||||
const encoding = 'utf-8';
|
||||
|
||||
// This is the true directory of the user-provided request handler in the
|
||||
// source files, so that's what we will use as an import path in the launcher.
|
||||
const locationPrefix = relative(entry, outputPath);
|
||||
|
||||
let handlerContent = await fs.readFile(fsPath, encoding);
|
||||
|
||||
const importPaths = [
|
||||
// This is the full entrypoint path, like `./api/test.py`. In our tests
|
||||
// Python didn't support importing from a parent directory without using different
|
||||
// code in the launcher that registers it as a location for modules and then changing
|
||||
// the importing syntax, but continuing to import it like before seems to work. If
|
||||
// other languages need this, we should consider excluding Python explicitly.
|
||||
// `./${entrypoint}`,
|
||||
|
||||
// This is the entrypoint path without extension, like `api/test`
|
||||
entrypoint.slice(0, -ext.length),
|
||||
];
|
||||
|
||||
// Generate a list of regular expressions that we can use for
|
||||
// finding matches, but only allow matches if the import path is
|
||||
// wrapped inside single (') or double quotes (").
|
||||
const patterns = importPaths.map(path => {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return new RegExp(`('|")(${path.replace(/\./g, '\\.')})('|")`, 'g');
|
||||
});
|
||||
|
||||
let replacedMatch = null;
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const newContent = handlerContent.replace(
|
||||
pattern,
|
||||
(_, p1, p2, p3) => {
|
||||
return `${p1}${join(locationPrefix, p2)}${p3}`;
|
||||
}
|
||||
);
|
||||
|
||||
if (newContent !== handlerContent) {
|
||||
debug(
|
||||
`Replaced "${pattern}" inside "${entry}" to ensure correct import of user-provided request handler`
|
||||
);
|
||||
|
||||
handlerContent = newContent;
|
||||
replacedMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!replacedMatch) {
|
||||
new Error(
|
||||
`No replacable matches for "${importPaths[0]}" or "${importPaths[1]}" found in "${fsPath}"`
|
||||
);
|
||||
}
|
||||
|
||||
await fs.writeFile(entry, handlerContent, encoding);
|
||||
} else {
|
||||
await fs.copy(handlerFile.fsPath, entry);
|
||||
}
|
||||
|
||||
// Legacy Runtimes based on interpreted languages will create a new launcher file
|
||||
// for every entrypoint, but they will create each one inside `workPath`, which means that
|
||||
// the launcher for one entrypoint will overwrite the launcher provided for the previous
|
||||
// entrypoint. That's why, above, we copy the file contents into the new destination (and
|
||||
// optionally transform them along the way), instead of linking. We then also want to remove
|
||||
// the copy origin right here, so that the `workPath` doesn't contain a useless launcher file
|
||||
// once the build has finished running.
|
||||
await fs.remove(handlerFile.fsPath);
|
||||
debug(`Removed temporary file "${handlerFile.fsPath}"`);
|
||||
|
||||
const nft = `${entry}.nft.json`;
|
||||
|
||||
const nft = join(
|
||||
workPath,
|
||||
'.output',
|
||||
'server',
|
||||
'pages',
|
||||
`${entrypoint}.nft.json`
|
||||
);
|
||||
const json = JSON.stringify({
|
||||
version: 1,
|
||||
files: tracedFiles.map(f => ({
|
||||
input: normalizePath(relative(nft, f.absolutePath)),
|
||||
output: normalizePath(f.relativePath),
|
||||
})),
|
||||
version: 2,
|
||||
files: Object.keys(lambdaFiles)
|
||||
.map(file => {
|
||||
const { fsPath } = lambdaFiles[file];
|
||||
|
||||
if (!fsPath) {
|
||||
throw new Error(
|
||||
`File "${file}" is missing valid \`fsPath\` property`
|
||||
);
|
||||
}
|
||||
|
||||
// The handler was already moved into position above.
|
||||
if (file === handlerFileBase) {
|
||||
return;
|
||||
}
|
||||
|
||||
return normalizePath(relative(dirname(nft), fsPath));
|
||||
})
|
||||
.filter(Boolean),
|
||||
});
|
||||
|
||||
await fs.ensureDir(dirname(nft));
|
||||
await fs.writeFile(nft, json);
|
||||
|
||||
// Add an entry that will later on be added to the `functions-manifest.json`
|
||||
// file that is placed inside of the `.output` directory.
|
||||
pages[normalizePath(entryPath)] = {
|
||||
// Because the underlying file used as a handler was placed
|
||||
// inside `.output/server/pages/api`, it no longer has the name it originally
|
||||
// had and is now named after the API Route that it's responsible for,
|
||||
// so we have to adjust the name of the Lambda handler accordingly.
|
||||
handler: handler.replace(handlerFileName, parse(entry).name),
|
||||
runtime: output.runtime,
|
||||
memory: output.memory,
|
||||
maxDuration: output.maxDuration,
|
||||
environment: output.environment,
|
||||
allowQuery: output.allowQuery,
|
||||
};
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
join(workPath, '.output', 'functions-manifest.json'),
|
||||
JSON.stringify(functionsManifest)
|
||||
);
|
||||
// Add any Serverless Functions that were exposed by the Legacy Runtime
|
||||
// to the `functions-manifest.json` file provided in `.output`.
|
||||
await updateFunctionsManifest({ workPath, pages });
|
||||
};
|
||||
}
|
||||
|
||||
async function linkOrCopy(existingPath: string, newPath: string) {
|
||||
async function readJson(filePath: string): Promise<{ [key: string]: any }> {
|
||||
try {
|
||||
await fs.createLink(existingPath, newPath);
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
await fs.copyFile(existingPath, newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readVercelConfigFunctions(
|
||||
workPath: string
|
||||
): Promise<BuilderFunctions> {
|
||||
const vercelJsonPath = join(workPath, 'vercel.json');
|
||||
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 +302,111 @@ async function readVercelConfigFunctions(
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `.output/functions-manifest.json` exists, append to the pages
|
||||
* property. Otherwise write a new file.
|
||||
*/
|
||||
export async function updateFunctionsManifest({
|
||||
workPath,
|
||||
pages,
|
||||
}: {
|
||||
workPath: string;
|
||||
pages: { [key: string]: any };
|
||||
}) {
|
||||
const functionsManifestPath = join(
|
||||
workPath,
|
||||
'.output',
|
||||
'functions-manifest.json'
|
||||
);
|
||||
const functionsManifest = await readJson(functionsManifestPath);
|
||||
|
||||
if (!functionsManifest.version) functionsManifest.version = 2;
|
||||
if (!functionsManifest.pages) functionsManifest.pages = {};
|
||||
|
||||
for (const [pageKey, pageConfig] of Object.entries(pages)) {
|
||||
functionsManifest.pages[pageKey] = { ...pageConfig };
|
||||
}
|
||||
|
||||
await fs.writeFile(functionsManifestPath, JSON.stringify(functionsManifest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Append routes to the `routes-manifest.json` file.
|
||||
* If the file does not exist, it will be created.
|
||||
*/
|
||||
export async function updateRoutesManifest({
|
||||
workPath,
|
||||
redirects,
|
||||
rewrites,
|
||||
headers,
|
||||
dynamicRoutes,
|
||||
staticRoutes,
|
||||
}: {
|
||||
workPath: string;
|
||||
redirects?: {
|
||||
source: string;
|
||||
destination: string;
|
||||
statusCode: number;
|
||||
regex: string;
|
||||
}[];
|
||||
rewrites?: {
|
||||
source: string;
|
||||
destination: string;
|
||||
regex: string;
|
||||
}[];
|
||||
headers?: {
|
||||
source: string;
|
||||
headers: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
regex: string;
|
||||
}[];
|
||||
dynamicRoutes?: {
|
||||
page: string;
|
||||
regex: string;
|
||||
namedRegex?: string;
|
||||
routeKeys?: { [named: string]: string };
|
||||
}[];
|
||||
staticRoutes?: {
|
||||
page: string;
|
||||
regex: string;
|
||||
namedRegex?: string;
|
||||
routeKeys?: { [named: string]: string };
|
||||
}[];
|
||||
}) {
|
||||
const routesManifestPath = join(workPath, '.output', 'routes-manifest.json');
|
||||
|
||||
const routesManifest = await readJson(routesManifestPath);
|
||||
|
||||
if (!routesManifest.version) routesManifest.version = 3;
|
||||
if (routesManifest.pages404 === undefined) routesManifest.pages404 = true;
|
||||
|
||||
if (redirects) {
|
||||
if (!routesManifest.redirects) routesManifest.redirects = [];
|
||||
routesManifest.redirects.push(...redirects);
|
||||
}
|
||||
|
||||
if (rewrites) {
|
||||
if (!routesManifest.rewrites) routesManifest.rewrites = [];
|
||||
routesManifest.rewrites.push(...rewrites);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
if (!routesManifest.headers) routesManifest.headers = [];
|
||||
routesManifest.headers.push(...headers);
|
||||
}
|
||||
|
||||
if (dynamicRoutes) {
|
||||
if (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = [];
|
||||
routesManifest.dynamicRoutes.push(...dynamicRoutes);
|
||||
}
|
||||
|
||||
if (staticRoutes) {
|
||||
if (!routesManifest.staticRoutes) routesManifest.staticRoutes = [];
|
||||
routesManifest.staticRoutes.push(...staticRoutes);
|
||||
}
|
||||
|
||||
await fs.writeFile(routesManifestPath, JSON.stringify(routesManifest));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,13 @@ import { valid as validSemver } from 'semver';
|
||||
import { parse as parsePath, extname } from 'path';
|
||||
import { Route, Source } from '@vercel/routing-utils';
|
||||
import frameworkList, { Framework } from '@vercel/frameworks';
|
||||
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
|
||||
import {
|
||||
PackageJson,
|
||||
Builder,
|
||||
Config,
|
||||
BuilderFunctions,
|
||||
ProjectSettings,
|
||||
} from './types';
|
||||
import { isOfficialRuntime } from './';
|
||||
const slugToFramework = new Map<string | null, Framework>(
|
||||
frameworkList.map(f => [f.slug, f])
|
||||
@@ -20,14 +26,7 @@ interface Options {
|
||||
tag?: 'canary' | 'latest' | string;
|
||||
functions?: BuilderFunctions;
|
||||
ignoreBuildScript?: boolean;
|
||||
projectSettings?: {
|
||||
framework?: string | null;
|
||||
devCommand?: string | null;
|
||||
installCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
createdAt?: number;
|
||||
};
|
||||
projectSettings?: ProjectSettings;
|
||||
cleanUrls?: boolean;
|
||||
trailingSlash?: boolean;
|
||||
featHandleMiss?: boolean;
|
||||
@@ -96,6 +95,7 @@ export async function detectBuilders(
|
||||
redirectRoutes: Route[] | null;
|
||||
rewriteRoutes: Route[] | null;
|
||||
errorRoutes: Route[] | null;
|
||||
limitedRoutes: LimitedRoutes | null;
|
||||
}> {
|
||||
const errors: ErrorResponse[] = [];
|
||||
const warnings: ErrorResponse[] = [];
|
||||
@@ -114,6 +114,7 @@ export async function detectBuilders(
|
||||
redirectRoutes: null,
|
||||
rewriteRoutes: null,
|
||||
errorRoutes: null,
|
||||
limitedRoutes: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -179,6 +180,7 @@ export async function detectBuilders(
|
||||
redirectRoutes: null,
|
||||
rewriteRoutes: null,
|
||||
errorRoutes: null,
|
||||
limitedRoutes: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -257,6 +259,7 @@ export async function detectBuilders(
|
||||
defaultRoutes: null,
|
||||
rewriteRoutes: null,
|
||||
errorRoutes: null,
|
||||
limitedRoutes: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -299,6 +302,7 @@ export async function detectBuilders(
|
||||
defaultRoutes: null,
|
||||
rewriteRoutes: null,
|
||||
errorRoutes: null,
|
||||
limitedRoutes: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -326,6 +330,7 @@ export async function detectBuilders(
|
||||
}
|
||||
|
||||
const routesResult = getRouteResult(
|
||||
pkg,
|
||||
apiRoutes,
|
||||
dynamicRoutes,
|
||||
usedOutputDirectory,
|
||||
@@ -342,6 +347,7 @@ export async function detectBuilders(
|
||||
defaultRoutes: routesResult.defaultRoutes,
|
||||
rewriteRoutes: routesResult.rewriteRoutes,
|
||||
errorRoutes: routesResult.errorRoutes,
|
||||
limitedRoutes: routesResult.limitedRoutes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -932,7 +938,14 @@ function createRouteFromPath(
|
||||
return { route, isDynamic };
|
||||
}
|
||||
|
||||
interface LimitedRoutes {
|
||||
defaultRoutes: Route[];
|
||||
redirectRoutes: Route[];
|
||||
rewriteRoutes: Route[];
|
||||
}
|
||||
|
||||
function getRouteResult(
|
||||
pkg: PackageJson | undefined | null,
|
||||
apiRoutes: Source[],
|
||||
dynamicRoutes: Source[],
|
||||
outputDirectory: string,
|
||||
@@ -944,11 +957,18 @@ function getRouteResult(
|
||||
redirectRoutes: Route[];
|
||||
rewriteRoutes: Route[];
|
||||
errorRoutes: Route[];
|
||||
limitedRoutes: LimitedRoutes;
|
||||
} {
|
||||
const deps = Object.assign({}, pkg?.dependencies, pkg?.devDependencies);
|
||||
const defaultRoutes: Route[] = [];
|
||||
const redirectRoutes: Route[] = [];
|
||||
const rewriteRoutes: Route[] = [];
|
||||
const errorRoutes: Route[] = [];
|
||||
const limitedRoutes: LimitedRoutes = {
|
||||
defaultRoutes: [],
|
||||
redirectRoutes: [],
|
||||
rewriteRoutes: [],
|
||||
};
|
||||
const framework = frontendBuilder?.config?.framework || '';
|
||||
const isNextjs =
|
||||
framework === 'nextjs' || isOfficialRuntime('next', frontendBuilder?.use);
|
||||
@@ -956,14 +976,43 @@ function getRouteResult(
|
||||
|
||||
if (apiRoutes && apiRoutes.length > 0) {
|
||||
if (options.featHandleMiss) {
|
||||
// Exclude extension names if the corresponding plugin is not found in package.json
|
||||
// detectBuilders({ignoreRoutesForBuilders: ['@vercel/python']})
|
||||
// return a copy of routes.
|
||||
// We should exclud errorRoutes and
|
||||
const extSet = detectApiExtensions(apiBuilders);
|
||||
const withTag = options.tag ? `@${options.tag}` : '';
|
||||
const extSetLimited = detectApiExtensions(
|
||||
apiBuilders.filter(b => {
|
||||
if (
|
||||
b.use === `@vercel/python${withTag}` &&
|
||||
!('vercel-plugin-python' in deps)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
b.use === `@vercel/go${withTag}` &&
|
||||
!('vercel-plugin-go' in deps)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
b.use === `@vercel/ruby${withTag}` &&
|
||||
!('vercel-plugin-ruby' in deps)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
if (extSet.size > 0) {
|
||||
const exts = Array.from(extSet)
|
||||
const extGroup = `(?:\\.(?:${Array.from(extSet)
|
||||
.map(ext => ext.slice(1))
|
||||
.join('|');
|
||||
|
||||
const extGroup = `(?:\\.(?:${exts}))`;
|
||||
.join('|')}))`;
|
||||
const extGroupLimited = `(?:\\.(?:${Array.from(extSetLimited)
|
||||
.map(ext => ext.slice(1))
|
||||
.join('|')}))`;
|
||||
|
||||
if (options.cleanUrls) {
|
||||
redirectRoutes.push({
|
||||
@@ -979,6 +1028,20 @@ function getRouteResult(
|
||||
},
|
||||
status: 308,
|
||||
});
|
||||
|
||||
limitedRoutes.redirectRoutes.push({
|
||||
src: `^/(api(?:.+)?)/index${extGroupLimited}?/?$`,
|
||||
headers: { Location: options.trailingSlash ? '/$1/' : '/$1' },
|
||||
status: 308,
|
||||
});
|
||||
|
||||
limitedRoutes.redirectRoutes.push({
|
||||
src: `^/api/(.+)${extGroupLimited}/?$`,
|
||||
headers: {
|
||||
Location: options.trailingSlash ? '/api/$1/' : '/api/$1',
|
||||
},
|
||||
status: 308,
|
||||
});
|
||||
} else {
|
||||
defaultRoutes.push({ handle: 'miss' });
|
||||
defaultRoutes.push({
|
||||
@@ -986,10 +1049,18 @@ function getRouteResult(
|
||||
dest: '/api/$1',
|
||||
check: true,
|
||||
});
|
||||
|
||||
limitedRoutes.defaultRoutes.push({ handle: 'miss' });
|
||||
limitedRoutes.defaultRoutes.push({
|
||||
src: `^/api/(.+)${extGroupLimited}$`,
|
||||
dest: '/api/$1',
|
||||
check: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rewriteRoutes.push(...dynamicRoutes);
|
||||
limitedRoutes.rewriteRoutes.push(...dynamicRoutes);
|
||||
|
||||
if (typeof ignoreRuntimes === 'undefined') {
|
||||
// This route is only necessary to hide the directory listing
|
||||
@@ -1040,6 +1111,7 @@ function getRouteResult(
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
errorRoutes,
|
||||
limitedRoutes,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
216
packages/build-utils/src/detect-file-system-api.ts
Normal file
216
packages/build-utils/src/detect-file-system-api.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import semver from 'semver';
|
||||
import { isOfficialRuntime } from './';
|
||||
import type {
|
||||
Builder,
|
||||
BuilderFunctions,
|
||||
PackageJson,
|
||||
ProjectSettings,
|
||||
} from './types';
|
||||
|
||||
interface Metadata {
|
||||
plugins: string[];
|
||||
hasDotOutput: boolean;
|
||||
hasMiddleware: boolean;
|
||||
}
|
||||
|
||||
const enableFileSystemApiFrameworks = new Set(['solidstart']);
|
||||
|
||||
/**
|
||||
* If the Deployment can be built with the new File System API,
|
||||
* we'll return the new Builder here, otherwise return `null`.
|
||||
*/
|
||||
export async function detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings,
|
||||
builders,
|
||||
vercelConfig,
|
||||
pkg,
|
||||
tag,
|
||||
enableFlag = false,
|
||||
}: {
|
||||
files: { [relPath: string]: any };
|
||||
projectSettings: ProjectSettings;
|
||||
builders: Builder[];
|
||||
vercelConfig:
|
||||
| { builds?: Builder[]; functions?: BuilderFunctions }
|
||||
| null
|
||||
| undefined;
|
||||
pkg: PackageJson | null | undefined;
|
||||
tag: string | undefined;
|
||||
enableFlag: boolean | undefined;
|
||||
}): Promise<
|
||||
| { metadata: Metadata; fsApiBuilder: Builder; reason: null }
|
||||
| { metadata: Metadata; fsApiBuilder: null; reason: string }
|
||||
> {
|
||||
const framework = projectSettings.framework || '';
|
||||
const deps = Object.assign({}, pkg?.dependencies, pkg?.devDependencies);
|
||||
const plugins = Object.keys(deps).filter(dep =>
|
||||
dep.startsWith('vercel-plugin-')
|
||||
);
|
||||
const hasDotOutput = Object.keys(files).some(file =>
|
||||
file.startsWith('.output/')
|
||||
);
|
||||
const hasMiddleware = Boolean(
|
||||
files['_middleware.js'] || files['_middleware.ts']
|
||||
);
|
||||
|
||||
const metadata: Metadata = {
|
||||
plugins,
|
||||
hasDotOutput,
|
||||
hasMiddleware,
|
||||
};
|
||||
|
||||
const isEnabled =
|
||||
enableFlag ||
|
||||
hasMiddleware ||
|
||||
hasDotOutput ||
|
||||
enableFileSystemApiFrameworks.has(framework);
|
||||
if (!isEnabled) {
|
||||
return { metadata, fsApiBuilder: null, reason: 'Flag not enabled.' };
|
||||
}
|
||||
|
||||
if (vercelConfig?.builds && vercelConfig.builds.length > 0) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `builds` in vercel.json. Please remove it in favor of CLI plugins.',
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.values(vercelConfig?.functions || {}).some(fn => !!fn.runtime)) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `functions.runtime` in vercel.json. Please remove it in favor of CLI plugins.',
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.HUGO_VERSION) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: 'Detected `HUGO_VERSION` environment variable. Please remove it.',
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.ZOLA_VERSION) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: 'Detected `ZOLA_VERSION` environment variable. Please remove it.',
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.GUTENBERG_VERSION) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `GUTENBERG_VERSION` environment variable. Please remove it.',
|
||||
};
|
||||
}
|
||||
|
||||
const invalidBuilder = builders.find(({ use }) => {
|
||||
const valid =
|
||||
isOfficialRuntime('go', use) ||
|
||||
isOfficialRuntime('python', use) ||
|
||||
isOfficialRuntime('ruby', use) ||
|
||||
isOfficialRuntime('node', use) ||
|
||||
isOfficialRuntime('next', use) ||
|
||||
isOfficialRuntime('static', use) ||
|
||||
isOfficialRuntime('static-build', use);
|
||||
return !valid;
|
||||
});
|
||||
|
||||
if (invalidBuilder) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected \`${invalidBuilder.use}\` in vercel.json. Please remove it in favor of CLI plugins.`,
|
||||
};
|
||||
}
|
||||
|
||||
for (const lang of ['go', 'python', 'ruby']) {
|
||||
for (const { use } of builders) {
|
||||
const plugin = 'vercel-plugin-' + lang;
|
||||
if (isOfficialRuntime(lang, use) && !deps[plugin]) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected \`${lang}\` Serverless Function usage without plugin \`${plugin}\`. Please run \`npm i ${plugin}\`.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
framework === 'nuxtjs' ||
|
||||
framework === 'sveltekit' ||
|
||||
framework === 'redwoodjs'
|
||||
) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected framework \`${framework}\` that only supports legacy File System API. Please contact the framework author.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (framework === 'nextjs' && !hasDotOutput) {
|
||||
// Use the old pipeline if a custom output directory was specified for Next.js
|
||||
// because `vercel build` cannot ensure that the directory will be in the same
|
||||
// location as `.output`, which can break imports (not just nft.json files).
|
||||
if (projectSettings?.outputDirectory) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected Next.js with Output Directory \`${projectSettings.outputDirectory}\` override. Please change it back to the default.`,
|
||||
};
|
||||
}
|
||||
const versionRange = deps['next'];
|
||||
if (!versionRange) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected Next.js in Project Settings but missing \`next\` package.json dependencies. Please run \`npm i next\`.`,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: We'll need to check the lockfile if one is present.
|
||||
if (versionRange !== 'latest' && versionRange !== 'canary') {
|
||||
const fixedVersion = semver.valid(semver.coerce(versionRange) || '');
|
||||
|
||||
if (!fixedVersion || !semver.gte(fixedVersion, '12.0.0')) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected legacy Next.js version "${versionRange}" in package.json. Please run \`npm i next@latest\` to upgrade.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frontendBuilder = builders.find(
|
||||
({ use }) =>
|
||||
isOfficialRuntime('next', use) ||
|
||||
isOfficialRuntime('static', use) ||
|
||||
isOfficialRuntime('static-build', use)
|
||||
);
|
||||
const config = frontendBuilder?.config || {};
|
||||
const withTag = tag ? `@${tag}` : '';
|
||||
|
||||
const fsApiBuilder = {
|
||||
use: `@vercelruntimes/file-system-api${withTag}`,
|
||||
src: '**',
|
||||
config: {
|
||||
...config,
|
||||
fileSystemAPI: true,
|
||||
framework: config.framework || framework || null,
|
||||
projectSettings,
|
||||
hasMiddleware,
|
||||
hasDotOutput,
|
||||
},
|
||||
};
|
||||
return { metadata, fsApiBuilder, reason: null };
|
||||
}
|
||||
84
packages/build-utils/src/get-ignore-filter.ts
Normal file
84
packages/build-utils/src/get-ignore-filter.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import ignore from 'ignore';
|
||||
|
||||
interface CodedError extends Error {
|
||||
code: string;
|
||||
}
|
||||
|
||||
function isCodedError(error: unknown): error is CodedError {
|
||||
return (
|
||||
error !== null &&
|
||||
error !== undefined &&
|
||||
(error as CodedError).code !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function clearRelative(s: string) {
|
||||
return s.replace(/(\n|^)\.\//g, '$1');
|
||||
}
|
||||
|
||||
export default async function (
|
||||
downloadPath: string,
|
||||
rootDirectory?: string | undefined
|
||||
) {
|
||||
const readFile = async (p: string) => {
|
||||
try {
|
||||
return await fs.readFile(p, 'utf8');
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error.code === 'ENOENT' ||
|
||||
(error instanceof Error && error.message.includes('ENOENT'))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const vercelIgnorePath = path.join(
|
||||
downloadPath,
|
||||
rootDirectory || '',
|
||||
'.vercelignore'
|
||||
);
|
||||
const nowIgnorePath = path.join(
|
||||
downloadPath,
|
||||
rootDirectory || '',
|
||||
'.nowignore'
|
||||
);
|
||||
const ignoreContents = [];
|
||||
|
||||
try {
|
||||
ignoreContents.push(
|
||||
...(
|
||||
await Promise.all([readFile(vercelIgnorePath), readFile(nowIgnorePath)])
|
||||
).filter(Boolean)
|
||||
);
|
||||
} catch (error) {
|
||||
if (isCodedError(error) && error.code === 'ENOTDIR') {
|
||||
console.log(`Warning: Cannot read ignore file from ${vercelIgnorePath}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreContents.length === 2) {
|
||||
throw new Error(
|
||||
'Cannot use both a `.vercelignore` and `.nowignore` file. Please delete the `.nowignore` file.'
|
||||
);
|
||||
}
|
||||
|
||||
if (ignoreContents.length === 0) {
|
||||
return () => false;
|
||||
}
|
||||
|
||||
const ignoreFilter: any = ignore().add(clearRelative(ignoreContents[0]!));
|
||||
|
||||
return function (p: string) {
|
||||
// we should not ignore now.json and vercel.json if it asked to.
|
||||
// we depend on these files for building the app with sourceless
|
||||
if (p === 'now.json' || p === 'vercel.json') return false;
|
||||
return ignoreFilter.test(p).ignored;
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from 'crypto';
|
||||
import FileBlob from './file-blob';
|
||||
import FileFsRef from './file-fs-ref';
|
||||
import FileRef from './file-ref';
|
||||
@@ -33,6 +34,7 @@ import { NowBuildError } from './errors';
|
||||
import streamToBuffer from './fs/stream-to-buffer';
|
||||
import shouldServe from './should-serve';
|
||||
import debug from './debug';
|
||||
import getIgnoreFilter from './get-ignore-filter';
|
||||
|
||||
export {
|
||||
FileBlob,
|
||||
@@ -70,6 +72,7 @@ export {
|
||||
isSymbolicLink,
|
||||
getLambdaOptionsFromFunction,
|
||||
scanParentDirs,
|
||||
getIgnoreFilter,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -78,11 +81,16 @@ export {
|
||||
detectApiDirectory,
|
||||
detectApiExtensions,
|
||||
} from './detect-builders';
|
||||
export { detectFileSystemAPI } from './detect-file-system-api';
|
||||
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';
|
||||
@@ -128,3 +136,11 @@ export const getPlatformEnv = (name: string): string | undefined => {
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for generating file or directories names in `.output/inputs`
|
||||
* for dependencies of files provided to the File System API.
|
||||
*/
|
||||
export const getInputHash = (source: Buffer | string): string => {
|
||||
return createHash('sha1').update(source).digest('hex');
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ interface CreateLambdaOptions {
|
||||
|
||||
interface GetLambdaOptionsFromFunctionOptions {
|
||||
sourceFile: string;
|
||||
config?: Config;
|
||||
config?: Pick<Config, 'functions'>;
|
||||
}
|
||||
|
||||
export const FILES_SYMBOL = Symbol('files');
|
||||
|
||||
@@ -29,7 +29,9 @@ export interface Config {
|
||||
| number
|
||||
| { [key: string]: string }
|
||||
| BuilderFunctions
|
||||
| undefined;
|
||||
| ProjectSettings
|
||||
| undefined
|
||||
| null;
|
||||
maxLambdaSize?: string;
|
||||
includeFiles?: string | string[];
|
||||
excludeFiles?: string | string[];
|
||||
@@ -41,11 +43,12 @@ export interface Config {
|
||||
zeroConfig?: boolean;
|
||||
import?: { [key: string]: string };
|
||||
functions?: BuilderFunctions;
|
||||
projectSettings?: ProjectSettings;
|
||||
outputDirectory?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
devCommand?: string;
|
||||
framework?: string;
|
||||
framework?: string | null;
|
||||
nodeVersion?: string;
|
||||
}
|
||||
|
||||
@@ -58,6 +61,7 @@ export interface Meta {
|
||||
filesRemoved?: string[];
|
||||
env?: Env;
|
||||
buildEnv?: Env;
|
||||
avoidTopLevelInstall?: boolean;
|
||||
}
|
||||
|
||||
export interface AnalyzeOptions {
|
||||
@@ -350,3 +354,17 @@ export interface BuilderFunctions {
|
||||
excludeFiles?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProjectSettings {
|
||||
framework?: string | null;
|
||||
devCommand?: string | null;
|
||||
installCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
createdAt?: number;
|
||||
autoExposeSystemEnvs?: boolean;
|
||||
sourceFilesOutsideRootDirectory?: boolean;
|
||||
directoryListing?: boolean;
|
||||
gitForkProtection?: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# users.rb
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/users/post.py": {
|
||||
"api/users.rb": {
|
||||
"memory": 3008
|
||||
},
|
||||
"api/not-matching-anything.py": {
|
||||
"api/doesnt-exist.rb": {
|
||||
"memory": 768
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# [id].py
|
||||
@@ -0,0 +1 @@
|
||||
# project/[aid]/[bid]/index.py
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/users/post.py": {
|
||||
"memory": 3008
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2385,13 +2385,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
{
|
||||
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
|
||||
|
||||
const { defaultRoutes, rewriteRoutes, errorRoutes } = await detectBuilders(
|
||||
files,
|
||||
null,
|
||||
{
|
||||
const { defaultRoutes, rewriteRoutes, errorRoutes, limitedRoutes } =
|
||||
await detectBuilders(files, null, {
|
||||
featHandleMiss,
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(defaultRoutes).toStrictEqual([
|
||||
{ handle: 'miss' },
|
||||
{
|
||||
@@ -2414,6 +2411,22 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
},
|
||||
]);
|
||||
|
||||
// Limited routes should have js but not go since the go plugin is not installed
|
||||
expect(limitedRoutes).toStrictEqual({
|
||||
redirectRoutes: [],
|
||||
rewriteRoutes: [],
|
||||
defaultRoutes: [
|
||||
{
|
||||
handle: 'miss',
|
||||
},
|
||||
{
|
||||
src: '^/api/(.+)(?:\\.(?:js))$',
|
||||
dest: '/api/$1',
|
||||
check: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const pattern = new RegExp(errorRoutes![0].src!);
|
||||
|
||||
[
|
||||
@@ -2816,8 +2829,13 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
|
||||
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes, errorRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
errorRoutes,
|
||||
limitedRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -2834,6 +2852,28 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
},
|
||||
]);
|
||||
|
||||
// Limited routes should have js but not go since the go plugin is not installed
|
||||
expect(limitedRoutes).toStrictEqual({
|
||||
redirectRoutes: [
|
||||
{
|
||||
src: '^/(api(?:.+)?)/index(?:\\.(?:js))?/?$',
|
||||
headers: {
|
||||
Location: '/$1',
|
||||
},
|
||||
status: 308,
|
||||
},
|
||||
{
|
||||
src: '^/api/(.+)(?:\\.(?:js))/?$',
|
||||
headers: {
|
||||
Location: '/api/$1',
|
||||
},
|
||||
status: 308,
|
||||
},
|
||||
],
|
||||
rewriteRoutes: [],
|
||||
defaultRoutes: [],
|
||||
});
|
||||
|
||||
// expected redirect should match inputs
|
||||
const getLocation = createReplaceLocation(redirectRoutes);
|
||||
|
||||
@@ -3077,7 +3117,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
|
||||
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes, limitedRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
@@ -3088,6 +3128,28 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
},
|
||||
]);
|
||||
|
||||
// Limited routes should have js but not go since the go plugin is not installed
|
||||
expect(limitedRoutes).toStrictEqual({
|
||||
redirectRoutes: [
|
||||
{
|
||||
src: '^/(api(?:.+)?)/index(?:\\.(?:js))?/?$',
|
||||
headers: {
|
||||
Location: '/$1/',
|
||||
},
|
||||
status: 308,
|
||||
},
|
||||
{
|
||||
src: '^/api/(.+)(?:\\.(?:js))/?$',
|
||||
headers: {
|
||||
Location: '/api/$1/',
|
||||
},
|
||||
status: 308,
|
||||
},
|
||||
],
|
||||
rewriteRoutes: [],
|
||||
defaultRoutes: [],
|
||||
});
|
||||
|
||||
// expected redirect should match inputs
|
||||
const getLocation = createReplaceLocation(redirectRoutes);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { BuildOptions, createLambda } from '../src';
|
||||
import { BuildOptions, createLambda, FileFsRef } from '../src';
|
||||
import { convertRuntimeToPlugin } from '../src/convert-runtime-to-plugin';
|
||||
|
||||
async function fsToJson(dir: string, output: Record<string, any> = {}) {
|
||||
@@ -18,24 +18,43 @@ async function fsToJson(dir: string, output: Record<string, any> = {}) {
|
||||
return output;
|
||||
}
|
||||
|
||||
const workPath = join(__dirname, 'walk', 'python-api');
|
||||
const invalidFuncWorkpath = join(
|
||||
__dirname,
|
||||
'convert-runtime',
|
||||
'invalid-functions'
|
||||
);
|
||||
const pythonApiWorkpath = join(__dirname, 'convert-runtime', 'python-api');
|
||||
|
||||
describe('convert-runtime-to-plugin', () => {
|
||||
afterEach(async () => {
|
||||
await fs.remove(join(workPath, '.output'));
|
||||
await fs.remove(join(invalidFuncWorkpath, '.output'));
|
||||
await fs.remove(join(pythonApiWorkpath, '.output'));
|
||||
});
|
||||
|
||||
it('should create correct fileystem for python', async () => {
|
||||
const ext = '.py';
|
||||
const workPath = pythonApiWorkpath;
|
||||
const handlerName = 'vc__handler__python';
|
||||
const handlerFileName = handlerName + ext;
|
||||
|
||||
const lambdaOptions = {
|
||||
handler: 'index.handler',
|
||||
handler: `${handlerName}.vc_handler`,
|
||||
runtime: 'python3.9',
|
||||
memory: 512,
|
||||
maxDuration: 5,
|
||||
environment: {},
|
||||
regions: ['sfo1'],
|
||||
};
|
||||
|
||||
const buildRuntime = async (opts: BuildOptions) => {
|
||||
const handlerPath = join(workPath, handlerFileName);
|
||||
|
||||
// This is the usual time at which a Legacy Runtime writes its Lambda launcher.
|
||||
await fs.writeFile(handlerPath, '# handler');
|
||||
|
||||
opts.files[handlerFileName] = new FileFsRef({
|
||||
fsPath: handlerPath,
|
||||
});
|
||||
|
||||
const lambda = await createLambda({
|
||||
files: opts.files,
|
||||
...lambdaOptions,
|
||||
@@ -43,25 +62,24 @@ describe('convert-runtime-to-plugin', () => {
|
||||
return { output: lambda };
|
||||
};
|
||||
|
||||
const lambdaFiles = await fsToJson(workPath);
|
||||
delete lambdaFiles['vercel.json'];
|
||||
const build = await convertRuntimeToPlugin(buildRuntime, '.py');
|
||||
const packageName = 'vercel-plugin-python';
|
||||
const build = await convertRuntimeToPlugin(buildRuntime, packageName, ext);
|
||||
|
||||
await build({ workPath });
|
||||
|
||||
const output = await fsToJson(join(workPath, '.output'));
|
||||
|
||||
expect(output).toMatchObject({
|
||||
'functions-manifest.json': expect.stringContaining('{'),
|
||||
'runtime-traced-files': lambdaFiles,
|
||||
server: {
|
||||
pages: {
|
||||
api: {
|
||||
'index.py': expect.stringContaining('index'),
|
||||
'index.py': expect.stringContaining('handler'),
|
||||
'index.py.nft.json': expect.stringContaining('{'),
|
||||
users: {
|
||||
'get.py': expect.stringContaining('get'),
|
||||
'get.py': expect.stringContaining('handler'),
|
||||
'get.py.nft.json': expect.stringContaining('{'),
|
||||
'post.py': expect.stringContaining('post'),
|
||||
'post.py': expect.stringContaining('handler'),
|
||||
'post.py.nft.json': expect.stringContaining('{'),
|
||||
},
|
||||
},
|
||||
@@ -71,39 +89,30 @@ 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: 2,
|
||||
pages: {
|
||||
'api/index.py': { ...lambdaOptions, handler: 'index.vc_handler' },
|
||||
'api/users/get.py': { ...lambdaOptions, handler: 'get.vc_handler' },
|
||||
'api/users/post.py': {
|
||||
...lambdaOptions,
|
||||
handler: 'post.vc_handler',
|
||||
memory: 512,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const indexJson = JSON.parse(output.server.pages.api['index.py.nft.json']);
|
||||
expect(indexJson).toMatchObject({
|
||||
version: 1,
|
||||
version: 2,
|
||||
files: [
|
||||
{
|
||||
input: '../../../../runtime-traced-files/api/index.py',
|
||||
output: 'api/index.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../runtime-traced-files/api/users/get.py',
|
||||
output: 'api/users/get.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../runtime-traced-files/api/users/post.py',
|
||||
output: 'api/users/post.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../runtime-traced-files/file.txt',
|
||||
output: 'file.txt',
|
||||
},
|
||||
{
|
||||
input: '../../../../runtime-traced-files/util/date.py',
|
||||
output: 'util/date.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../runtime-traced-files/util/math.py',
|
||||
output: 'util/math.py',
|
||||
},
|
||||
'../../../../api/db/[id].py',
|
||||
'../../../../api/index.py',
|
||||
'../../../../api/project/[aid]/[bid]/index.py',
|
||||
'../../../../api/users/get.py',
|
||||
'../../../../api/users/post.py',
|
||||
'../../../../file.txt',
|
||||
'../../../../util/date.py',
|
||||
'../../../../util/math.py',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -111,32 +120,16 @@ describe('convert-runtime-to-plugin', () => {
|
||||
output.server.pages.api.users['get.py.nft.json']
|
||||
);
|
||||
expect(getJson).toMatchObject({
|
||||
version: 1,
|
||||
version: 2,
|
||||
files: [
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/index.py',
|
||||
output: 'api/index.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/users/get.py',
|
||||
output: 'api/users/get.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/users/post.py',
|
||||
output: 'api/users/post.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/file.txt',
|
||||
output: 'file.txt',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/util/date.py',
|
||||
output: 'util/date.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/util/math.py',
|
||||
output: 'util/math.py',
|
||||
},
|
||||
'../../../../../api/db/[id].py',
|
||||
'../../../../../api/index.py',
|
||||
'../../../../../api/project/[aid]/[bid]/index.py',
|
||||
'../../../../../api/users/get.py',
|
||||
'../../../../../api/users/post.py',
|
||||
'../../../../../file.txt',
|
||||
'../../../../../util/date.py',
|
||||
'../../../../../util/math.py',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -144,32 +137,16 @@ describe('convert-runtime-to-plugin', () => {
|
||||
output.server.pages.api.users['post.py.nft.json']
|
||||
);
|
||||
expect(postJson).toMatchObject({
|
||||
version: 1,
|
||||
version: 2,
|
||||
files: [
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/index.py',
|
||||
output: 'api/index.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/users/get.py',
|
||||
output: 'api/users/get.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/users/post.py',
|
||||
output: 'api/users/post.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/file.txt',
|
||||
output: 'file.txt',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/util/date.py',
|
||||
output: 'util/date.py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/util/math.py',
|
||||
output: 'util/math.py',
|
||||
},
|
||||
'../../../../../api/db/[id].py',
|
||||
'../../../../../api/index.py',
|
||||
'../../../../../api/project/[aid]/[bid]/index.py',
|
||||
'../../../../../api/users/get.py',
|
||||
'../../../../../api/users/post.py',
|
||||
'../../../../../file.txt',
|
||||
'../../../../../util/date.py',
|
||||
'../../../../../util/math.py',
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
427
packages/build-utils/test/unit.detect-file-system-api.test.ts
vendored
Normal file
427
packages/build-utils/test/unit.detect-file-system-api.test.ts
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
import { detectFileSystemAPI } from '../src/detect-file-system-api';
|
||||
|
||||
describe('Test `detectFileSystemAPI`', () => {
|
||||
it('should error when builds in vercel.json', async () => {
|
||||
const vercelConfig = {
|
||||
builds: [{ use: '@vercel/node', src: 'api/**/*.js' }],
|
||||
};
|
||||
const files = {
|
||||
'vercel.json': JSON.stringify(vercelConfig),
|
||||
'api/foo.js': 'console.log("foo")',
|
||||
};
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: vercelConfig.builds,
|
||||
vercelConfig,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `builds` in vercel.json. Please remove it in favor of CLI plugins.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when functions.runtimes in vercel.json', async () => {
|
||||
const vercelConfig = {
|
||||
functions: {
|
||||
'api/**/*.rs': {
|
||||
runtime: 'vercel-rust@latest',
|
||||
},
|
||||
},
|
||||
};
|
||||
const files = {
|
||||
'vercel.json': JSON.stringify(vercelConfig),
|
||||
'api/foo.rs': 'println!("foo")',
|
||||
};
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: [],
|
||||
vercelConfig,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `functions.runtime` in vercel.json. Please remove it in favor of CLI plugins.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when HUGO_VERSION env var used', async () => {
|
||||
process.env.HUGO_VERSION = 'v0.58.2';
|
||||
const files = { 'foo.html': '<h1>Foo</h1>' };
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: [],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason: 'Detected `HUGO_VERSION` environment variable. Please remove it.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
delete process.env.HUGO_VERSION;
|
||||
});
|
||||
|
||||
it('should error when ZOLA_VERSION env var used', async () => {
|
||||
process.env.ZOLA_VERSION = 'v0.0.1';
|
||||
const files = { 'foo.html': '<h1>Foo</h1>' };
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: [],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason: 'Detected `ZOLA_VERSION` environment variable. Please remove it.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
delete process.env.ZOLA_VERSION;
|
||||
});
|
||||
|
||||
it('should error when GUTENBERG_VERSION env var used', async () => {
|
||||
process.env.GUTENBERG_VERSION = 'v0.0.1';
|
||||
const files = { 'foo.html': '<h1>Foo</h1>' };
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: [],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `GUTENBERG_VERSION` environment variable. Please remove it.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
delete process.env.GUTENBERG_VERSION;
|
||||
});
|
||||
|
||||
it('should error when Go detected without corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.go': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/go', src: 'api/**/*.go' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `go` Serverless Function usage without plugin `vercel-plugin-go`. Please run `npm i vercel-plugin-go`.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when Python detected without corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.py': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/python', src: 'api/**/*.py' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `python` Serverless Function usage without plugin `vercel-plugin-python`. Please run `npm i vercel-plugin-python`.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when Ruby detected without corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.rb': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/ruby', src: 'api/**/*.rb' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `ruby` Serverless Function usage without plugin `vercel-plugin-ruby`. Please run `npm i vercel-plugin-ruby`.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when Go detected with corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.go': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/go', src: 'api/**/*.go' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { 'vercel-plugin-go': '^1.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: null,
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
projectSettings: {},
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: {
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
plugins: ['vercel-plugin-go'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when Python detected with corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.py': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/python', src: 'api/**/*.py' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { 'vercel-plugin-python': '^1.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: null,
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
projectSettings: {},
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: {
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
plugins: ['vercel-plugin-python'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when Ruby detected with corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.rb': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/ruby', src: 'api/**/*.rb' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { 'vercel-plugin-ruby': '^1.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: null,
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
projectSettings: {},
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: {
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
plugins: ['vercel-plugin-ruby'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is nuxtjs', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nuxtjs' },
|
||||
builders: [{ use: '@vercel/node', src: 'api/**/*.js' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected framework `nuxtjs` that only supports legacy File System API. Please contact the framework author.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is sveltekit', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'sveltekit' },
|
||||
builders: [{ use: '@vercel/node', src: 'api/**/*.js' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected framework `sveltekit` that only supports legacy File System API. Please contact the framework author.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is redwoodjs', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'redwoodjs' },
|
||||
builders: [{ use: '@vercel/node', src: 'api/**/*.js' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected framework `redwoodjs` that only supports legacy File System API. Please contact the framework author.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is nextjs and has output dir', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'pages/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nextjs', outputDirectory: 'dist' },
|
||||
builders: [{ use: '@vercel/next', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { next: '^12.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected Next.js with Output Directory `dist` override. Please change it back to the default.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is nextjs but missing from dependencies', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'pages/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nextjs' },
|
||||
builders: [{ use: '@vercel/next', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { 'not-next': '^12.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected Next.js in Project Settings but missing `next` package.json dependencies. Please run `npm i next`.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is nextjs but dependency is older version', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'pages/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nextjs' },
|
||||
builders: [{ use: '@vercel/next', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { next: '^9.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected legacy Next.js version "^9.0.0" in package.json. Please run `npm i next@latest` to upgrade.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when middleware detected', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { '_middleware.js': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/static-build', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: false,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: null,
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: true,
|
||||
projectSettings: {},
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: { hasDotOutput: false, hasMiddleware: true, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when .output detected', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { '.output/routes-manifest.json': '{}' },
|
||||
projectSettings: { framework: 'remix' },
|
||||
builders: [{ use: '@vercel/static-build', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: false,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: 'remix',
|
||||
hasDotOutput: true,
|
||||
hasMiddleware: false,
|
||||
projectSettings: { framework: 'remix' },
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: { hasDotOutput: true, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,7 @@ Finally, [connect your Git repository to Vercel](https://vercel.com/docs/git) an
|
||||
|
||||
## Documentation
|
||||
|
||||
For details on how to use Vercel CLI, check out our [documentation](https://vercel.com/docs).
|
||||
For details on how to use Vercel CLI, check out our [documentation](https://vercel.com/docs/cli).
|
||||
|
||||
## Local Development
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "23.1.3-canary.28",
|
||||
"version": "23.1.3-canary.68",
|
||||
"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/ruby": "1.2.8-canary.4",
|
||||
"@vercel/build-utils": "2.12.3-canary.43",
|
||||
"@vercel/go": "1.2.4-canary.4",
|
||||
"@vercel/node": "1.12.2-canary.7",
|
||||
"@vercel/python": "2.1.2-canary.2",
|
||||
"@vercel/ruby": "1.2.10-canary.0",
|
||||
"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.20",
|
||||
"vercel-plugin-node": "1.12.2-canary.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/env": "11.1.2",
|
||||
@@ -90,7 +90,8 @@
|
||||
"@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/client": "10.2.3-canary.46",
|
||||
"@vercel/frameworks": "0.5.1-canary.17",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.17.0",
|
||||
"@zeit/fun": "0.11.2",
|
||||
|
||||
@@ -5,19 +5,24 @@ import {
|
||||
GlobOptions,
|
||||
scanParentDirs,
|
||||
spawnAsync,
|
||||
glob as buildUtilsGlob,
|
||||
detectFileSystemAPI,
|
||||
detectBuilders,
|
||||
PackageJson,
|
||||
} from '@vercel/build-utils';
|
||||
import { nodeFileTrace } from '@vercel/nft';
|
||||
import Sema from 'async-sema';
|
||||
import chalk from 'chalk';
|
||||
import { SpawnOptions } from 'child_process';
|
||||
import { assert } from 'console';
|
||||
import { createHash } from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import ogGlob from 'glob';
|
||||
import { isAbsolute, join, parse, relative, resolve } from 'path';
|
||||
import { dirname, isAbsolute, join, parse, relative } from 'path';
|
||||
import pluralize from 'pluralize';
|
||||
import Client from '../util/client';
|
||||
import { VercelConfig } from '../util/dev/types';
|
||||
import { emoji, prependEmoji } from '../util/emoji';
|
||||
import { CantParseJSONFile } from '../util/errors-ts';
|
||||
import getArgs from '../util/get-args';
|
||||
import handleError from '../util/handle-error';
|
||||
import confirm from '../util/input/confirm';
|
||||
@@ -30,10 +35,8 @@ 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 readJSONFile from '../util/read-json-file';
|
||||
import pull from './pull';
|
||||
|
||||
const sema = new Sema(16, {
|
||||
@@ -68,16 +71,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 {
|
||||
@@ -131,14 +141,46 @@ export default async function main(client: Client) {
|
||||
});
|
||||
|
||||
// Set process.env with loaded environment variables
|
||||
await processEnv(loadedEnvFiles);
|
||||
processEnv(loadedEnvFiles);
|
||||
|
||||
const spawnOpts = {
|
||||
const spawnOpts: {
|
||||
env: Record<string, string | undefined>;
|
||||
} = {
|
||||
env: { ...combinedEnv, VERCEL: '1' },
|
||||
};
|
||||
|
||||
process.chdir(cwd);
|
||||
|
||||
const pkg = await readJSONFile<PackageJson>('./package.json');
|
||||
if (pkg instanceof CantParseJSONFile) {
|
||||
throw pkg;
|
||||
}
|
||||
const vercelConfig = await readJSONFile<VercelConfig>('./vercel.json');
|
||||
if (vercelConfig instanceof CantParseJSONFile) {
|
||||
throw vercelConfig;
|
||||
}
|
||||
|
||||
if (!process.env.NOW_BUILDER) {
|
||||
// This validation is only necessary when
|
||||
// a user runs `vercel build` locally.
|
||||
const globFiles = await buildUtilsGlob('**', { cwd });
|
||||
const zeroConfig = await detectBuilders(Object.keys(globFiles), pkg);
|
||||
const { reason } = await detectFileSystemAPI({
|
||||
files: globFiles,
|
||||
projectSettings: project.settings,
|
||||
builders: zeroConfig.builders || [],
|
||||
pkg,
|
||||
vercelConfig,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
|
||||
if (reason) {
|
||||
client.output.error(`${cmd(`${getPkgName()} build`)} failed: ${reason}`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const framework = findFramework(project.settings.framework);
|
||||
// If this is undefined, we bail. If it is null, then findFramework should return "Other",
|
||||
// so this should really never happen, but just in case....
|
||||
@@ -152,47 +194,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 +275,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 +283,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 +321,34 @@ export default async function main(client: Client) {
|
||||
// Clean the output directory
|
||||
fs.removeSync(join(cwd, OUTPUT_DIR));
|
||||
|
||||
if (framework && process.env.VERCEL_URL && 'envPrefix' in framework) {
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (key.startsWith('VERCEL_')) {
|
||||
const newKey = `${framework.envPrefix}${key}`;
|
||||
// Set `process.env` and `spawnOpts.env` to make sure the variables are
|
||||
// available to the `build` step and the CLI Plugins.
|
||||
process.env[newKey] = process.env[newKey] || process.env[key];
|
||||
spawnOpts.env[newKey] = process.env[newKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Required for Next.js to produce the correct `.nft.json` files.
|
||||
spawnOpts.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = baseDir;
|
||||
|
||||
// Yarn v2 PnP mode may be activated, so force
|
||||
// "node-modules" linker style
|
||||
const env = {
|
||||
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,21 +357,58 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
if (!fs.existsSync(join(cwd, OUTPUT_DIR))) {
|
||||
let outputDir = join(OUTPUT_DIR, 'static');
|
||||
let distDir = await framework.getFsOutputDir(cwd);
|
||||
if (framework.slug === 'nextjs') {
|
||||
outputDir = OUTPUT_DIR;
|
||||
let dotNextDir: string | null = null;
|
||||
|
||||
// If a custom `outputDirectory` was set, we'll need to verify
|
||||
// if it's `.next` output, or just static output.
|
||||
const userOutputDirectory = project.settings.outputDirectory;
|
||||
|
||||
if (typeof userOutputDirectory === 'string') {
|
||||
if (fs.existsSync(join(cwd, userOutputDirectory, 'BUILD_ID'))) {
|
||||
dotNextDir = join(cwd, userOutputDirectory);
|
||||
client.output.debug(
|
||||
`Consider ${param(userOutputDirectory)} as ${param('.next')} output.`
|
||||
);
|
||||
}
|
||||
} else if (fs.existsSync(join(cwd, '.next'))) {
|
||||
dotNextDir = join(cwd, '.next');
|
||||
client.output.debug(`Found ${param('.next')} directory.`);
|
||||
}
|
||||
const copyStamp = stamp();
|
||||
|
||||
// We cannot rely on the `framework` alone, as it might be a static export,
|
||||
// and the current build might use a different project that's not in the settings.
|
||||
const isNextOutput = Boolean(dotNextDir);
|
||||
const nextExport = await getNextExportStatus(dotNextDir);
|
||||
const outputDir =
|
||||
isNextOutput && !nextExport ? OUTPUT_DIR : join(OUTPUT_DIR, 'static');
|
||||
const getDistDir = framework.getFsOutputDir || framework.getOutputDirName;
|
||||
const distDir =
|
||||
(nextExport?.exportDetail.outDirectory
|
||||
? relative(cwd, nextExport.exportDetail.outDirectory)
|
||||
: false) ||
|
||||
dotNextDir ||
|
||||
userOutputDirectory ||
|
||||
(await getDistDir(cwd));
|
||||
|
||||
await fs.ensureDir(join(cwd, outputDir));
|
||||
const relativeDistDir = relative(cwd, distDir);
|
||||
|
||||
const copyStamp = stamp();
|
||||
client.output.spinner(
|
||||
`Copying files from ${param(distDir)} to ${param(outputDir)}`
|
||||
);
|
||||
const files = await glob(join(relativeDistDir, '**'), {
|
||||
const files = await glob(join(relative(cwd, distDir), '**'), {
|
||||
ignore: [
|
||||
'node_modules/**',
|
||||
'.vercel/**',
|
||||
@@ -336,7 +442,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()}`
|
||||
@@ -350,6 +456,7 @@ export default async function main(client: Client) {
|
||||
`Generating build manifest: ${param(buildManifestPath)}`
|
||||
);
|
||||
const buildManifest = {
|
||||
version: 1,
|
||||
cache: framework.cachePattern ? [framework.cachePattern] : [],
|
||||
};
|
||||
await fs.writeJSON(buildManifestPath, buildManifest, { spaces: 2 });
|
||||
@@ -377,7 +484,53 @@ export default async function main(client: Client) {
|
||||
}
|
||||
|
||||
// Special Next.js processing.
|
||||
if (framework.slug === 'nextjs') {
|
||||
if (nextExport) {
|
||||
client.output.debug('Found `next export` output.');
|
||||
|
||||
const htmlFiles = await buildUtilsGlob(
|
||||
'**/*.html',
|
||||
join(cwd, OUTPUT_DIR, 'static')
|
||||
);
|
||||
|
||||
if (nextExport.exportDetail.success !== true) {
|
||||
client.output.error(
|
||||
`Export of Next.js app failed. Please check your build logs.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await fs.mkdirp(join(cwd, OUTPUT_DIR, 'server', 'pages'));
|
||||
await fs.mkdirp(join(cwd, OUTPUT_DIR, 'static'));
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(htmlFiles).map(async fileName => {
|
||||
await sema.acquire();
|
||||
|
||||
const input = join(cwd, OUTPUT_DIR, 'static', fileName);
|
||||
const target = join(cwd, OUTPUT_DIR, 'server', 'pages', fileName);
|
||||
|
||||
await fs.mkdirp(dirname(target));
|
||||
|
||||
await fs.promises.rename(input, target).finally(() => {
|
||||
sema.release();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
for (const file of [
|
||||
'BUILD_ID',
|
||||
'images-manifest.json',
|
||||
'routes-manifest.json',
|
||||
'build-manifest.json',
|
||||
]) {
|
||||
const input = join(nextExport.dotNextDir, file);
|
||||
|
||||
if (fs.existsSync(input)) {
|
||||
// Do not use `smartCopy`, since we want to overwrite if they already exist.
|
||||
await fs.copyFile(input, join(OUTPUT_DIR, file));
|
||||
}
|
||||
}
|
||||
} else if (isNextOutput) {
|
||||
// The contents of `.output/static` should be placed inside of `.output/static/_next/static`
|
||||
const tempStatic = '___static';
|
||||
await fs.rename(
|
||||
@@ -428,10 +581,12 @@ export default async function main(client: Client) {
|
||||
// `public`, then`static`). We can't read both at the same time because that would mean we'd
|
||||
// read public for old Next.js versions that don't support it, which might be breaking (and
|
||||
// we don't want to make vercel build specific framework versions).
|
||||
const nextSrcDirectory = dirname(distDir);
|
||||
|
||||
const publicFiles = await glob('public/**', {
|
||||
nodir: true,
|
||||
dot: true,
|
||||
cwd,
|
||||
cwd: nextSrcDirectory,
|
||||
absolute: true,
|
||||
});
|
||||
if (publicFiles.length > 0) {
|
||||
@@ -440,7 +595,11 @@ export default async function main(client: Client) {
|
||||
smartCopy(
|
||||
client,
|
||||
f,
|
||||
f.replace('public', join(OUTPUT_DIR, 'static'))
|
||||
join(
|
||||
OUTPUT_DIR,
|
||||
'static',
|
||||
relative(join(dirname(distDir), 'public'), f)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -448,7 +607,7 @@ export default async function main(client: Client) {
|
||||
const staticFiles = await glob('static/**', {
|
||||
nodir: true,
|
||||
dot: true,
|
||||
cwd,
|
||||
cwd: nextSrcDirectory,
|
||||
absolute: true,
|
||||
});
|
||||
await Promise.all(
|
||||
@@ -456,7 +615,12 @@ export default async function main(client: Client) {
|
||||
smartCopy(
|
||||
client,
|
||||
f,
|
||||
f.replace('static', join(OUTPUT_DIR, 'static', 'static'))
|
||||
join(
|
||||
OUTPUT_DIR,
|
||||
'static',
|
||||
'static',
|
||||
relative(join(dirname(distDir), 'static'), f)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -475,6 +639,7 @@ export default async function main(client: Client) {
|
||||
const nftFiles = await glob(join(OUTPUT_DIR, '**', '*.nft.json'), {
|
||||
nodir: true,
|
||||
dot: true,
|
||||
ignore: ['cache/**'],
|
||||
cwd,
|
||||
absolute: true,
|
||||
});
|
||||
@@ -506,28 +671,15 @@ export default async function main(client: Client) {
|
||||
],
|
||||
});
|
||||
fileList.delete(relative(cwd, f));
|
||||
await resolveNftToOutput({
|
||||
client,
|
||||
baseDir,
|
||||
outputDir: OUTPUT_DIR,
|
||||
nftFileName: f.replace(ext, '.js.nft.json'),
|
||||
nft: {
|
||||
version: 1,
|
||||
files: Array.from(fileList).map(fileListEntry =>
|
||||
relative(dir, fileListEntry)
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (let f of nftFiles) {
|
||||
const json = await fs.readJson(f);
|
||||
await resolveNftToOutput({
|
||||
client,
|
||||
baseDir,
|
||||
outputDir: OUTPUT_DIR,
|
||||
nftFileName: f,
|
||||
nft: json,
|
||||
|
||||
const nftFileName = f.replace(ext, '.js.nft.json');
|
||||
client.output.debug(`Creating ${nftFileName}`);
|
||||
|
||||
await fs.writeJSON(nftFileName, {
|
||||
version: 2,
|
||||
files: Array.from(fileList).map(fileListEntry =>
|
||||
relative(dir, fileListEntry)
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -536,33 +688,44 @@ export default async function main(client: Client) {
|
||||
OUTPUT_DIR,
|
||||
'required-server-files.json'
|
||||
);
|
||||
const requiredServerFilesJson = await fs.readJSON(
|
||||
requiredServerFilesPath
|
||||
);
|
||||
await fs.writeJSON(requiredServerFilesPath, {
|
||||
...requiredServerFilesJson,
|
||||
appDir: '.',
|
||||
files: requiredServerFilesJson.files.map((i: string) => {
|
||||
const absolutePath = join(cwd, i.replace('.next', '.output'));
|
||||
const output = relative(baseDir, absolutePath);
|
||||
|
||||
return {
|
||||
input: i.replace('.next', '.output'),
|
||||
output,
|
||||
};
|
||||
}),
|
||||
});
|
||||
if (fs.existsSync(requiredServerFilesPath)) {
|
||||
client.output.debug(`Resolve ${param('required-server-files.json')}.`);
|
||||
|
||||
const requiredServerFilesJson = await fs.readJSON(
|
||||
requiredServerFilesPath
|
||||
);
|
||||
|
||||
await fs.writeJSON(requiredServerFilesPath, {
|
||||
...requiredServerFilesJson,
|
||||
appDir: '.',
|
||||
files: requiredServerFilesJson.files.map((i: string) => {
|
||||
const originalPath = join(requiredServerFilesJson.appDir, i);
|
||||
const relPath = join(OUTPUT_DIR, relative(distDir, originalPath));
|
||||
|
||||
return relPath;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +738,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 +756,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 +808,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,70 +861,51 @@ async function glob(pattern: string, options: GlobOptions): Promise<string[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash for the given buf.
|
||||
*
|
||||
* @param {Buffer} file data
|
||||
* @return {String} hex digest
|
||||
* Files will only exist when `next export` was used.
|
||||
*/
|
||||
function hash(buf: Buffer): string {
|
||||
return createHash('sha1').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
interface NftFile {
|
||||
version: number;
|
||||
files: (string | { input: string; output: string })[];
|
||||
}
|
||||
|
||||
// resolveNftToOutput takes nft file and moves all of its trace files
|
||||
// into the specified directory + `inputs`, (renaming them to their hash + ext) and
|
||||
// subsequently updating the original nft file accordingly. This is done
|
||||
// to make the `.output` directory be self-contained, so that it works
|
||||
// properly with `vc --prebuilt`.
|
||||
async function resolveNftToOutput({
|
||||
client,
|
||||
baseDir,
|
||||
outputDir,
|
||||
nftFileName,
|
||||
nft,
|
||||
}: {
|
||||
client: Client;
|
||||
baseDir: string;
|
||||
outputDir: string;
|
||||
nftFileName: string;
|
||||
nft: NftFile;
|
||||
}) {
|
||||
client.output.debug(`Processing and resolving ${nftFileName}`);
|
||||
await fs.ensureDir(join(outputDir, 'inputs'));
|
||||
const newFilesList: NftFile['files'] = [];
|
||||
for (let fileEntity of nft.files) {
|
||||
const relativeInput: string =
|
||||
typeof fileEntity === 'string' ? fileEntity : fileEntity.input;
|
||||
const fullInput = resolve(join(parse(nftFileName).dir, relativeInput));
|
||||
|
||||
// if the resolved path is NOT in the .output directory we move in it there
|
||||
if (!fullInput.includes(outputDir)) {
|
||||
const { ext } = parse(fullInput);
|
||||
const raw = await fs.readFile(fullInput);
|
||||
const newFilePath = join(outputDir, 'inputs', hash(raw) + ext);
|
||||
smartCopy(client, fullInput, newFilePath);
|
||||
|
||||
// We have to use `baseDir` instead of `cwd`, because we want to
|
||||
// mount everything from there (especially `node_modules`).
|
||||
// This is important for NPM Workspaces where `node_modules` is not
|
||||
// in the directory of the workspace.
|
||||
const output = relative(baseDir, fullInput).replace('.output', '.next');
|
||||
|
||||
newFilesList.push({
|
||||
input: relative(parse(nftFileName).dir, newFilePath),
|
||||
output,
|
||||
});
|
||||
} else {
|
||||
newFilesList.push(relativeInput);
|
||||
}
|
||||
async function getNextExportStatus(dotNextDir: string | null) {
|
||||
if (!dotNextDir) {
|
||||
return null;
|
||||
}
|
||||
// Update the .nft.json with new input and output mapping
|
||||
await fs.writeJSON(nftFileName, {
|
||||
...nft,
|
||||
files: newFilesList,
|
||||
});
|
||||
|
||||
const exportDetail: {
|
||||
success: boolean;
|
||||
outDirectory: string;
|
||||
} | null = await fs
|
||||
.readJson(join(dotNextDir, 'export-detail.json'))
|
||||
.catch(error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
if (!exportDetail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exportMarker: {
|
||||
version: 1;
|
||||
exportTrailingSlash: boolean;
|
||||
hasExportPathMap: boolean;
|
||||
} | null = await fs
|
||||
.readJSON(join(dotNextDir, 'export-marker.json'))
|
||||
.catch(error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
return {
|
||||
dotNextDir,
|
||||
exportDetail,
|
||||
exportMarker: {
|
||||
trailingSlash: exportMarker?.hasExportPathMap
|
||||
? exportMarker.exportTrailingSlash
|
||||
: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,8 +166,8 @@ export default async (client: Client) => {
|
||||
return pathValidation.exitCode;
|
||||
}
|
||||
|
||||
const { isFile, path } = pathValidation;
|
||||
const autoConfirm = argv['--confirm'] || isFile;
|
||||
const { path } = pathValidation;
|
||||
const autoConfirm = argv['--confirm'];
|
||||
|
||||
// deprecate --name
|
||||
if (argv['--name']) {
|
||||
@@ -192,7 +192,7 @@ export default async (client: Client) => {
|
||||
|
||||
let newProjectName = null;
|
||||
let rootDirectory = project ? project.rootDirectory : null;
|
||||
let sourceFilesOutsideRootDirectory = true;
|
||||
let sourceFilesOutsideRootDirectory: boolean | undefined = true;
|
||||
|
||||
if (status === 'not_linked') {
|
||||
const shouldStartSetup =
|
||||
@@ -229,8 +229,7 @@ export default async (client: Client) => {
|
||||
// user input.
|
||||
const detectedProjectName = getProjectName({
|
||||
argv,
|
||||
nowConfig: localConfig || {},
|
||||
isFile,
|
||||
nowConfig: localConfig,
|
||||
paths,
|
||||
});
|
||||
|
||||
@@ -447,9 +446,9 @@ export default async (client: Client) => {
|
||||
forceNew: argv['--force'],
|
||||
withCache: argv['--with-cache'],
|
||||
prebuilt: argv['--prebuilt'],
|
||||
rootDirectory,
|
||||
quiet,
|
||||
wantsPublic: argv['--public'] || localConfig.public,
|
||||
isFile,
|
||||
type: null,
|
||||
nowConfig: localConfig,
|
||||
regions,
|
||||
@@ -471,7 +470,7 @@ export default async (client: Client) => {
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
!project && !isFile,
|
||||
!project,
|
||||
path
|
||||
);
|
||||
|
||||
@@ -654,8 +653,7 @@ export default async (client: Client) => {
|
||||
client,
|
||||
deployment,
|
||||
deployStamp,
|
||||
!argv['--no-clipboard'],
|
||||
isFile
|
||||
!argv['--no-clipboard']
|
||||
);
|
||||
};
|
||||
|
||||
@@ -790,8 +788,7 @@ const printDeploymentStatus = async (
|
||||
};
|
||||
},
|
||||
deployStamp: () => string,
|
||||
isClipboardEnabled: boolean,
|
||||
isFile: boolean
|
||||
isClipboardEnabled: boolean
|
||||
) => {
|
||||
indications = indications || [];
|
||||
const isProdDeployment = target === 'production';
|
||||
@@ -813,7 +810,7 @@ const printDeploymentStatus = async (
|
||||
// print preview/production url
|
||||
let previewUrl: string;
|
||||
let isWildcard: boolean;
|
||||
if (!isFile && Array.isArray(aliasList) && aliasList.length > 0) {
|
||||
if (Array.isArray(aliasList) && aliasList.length > 0) {
|
||||
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
|
||||
if (previewUrlInfo) {
|
||||
isWildcard = previewUrlInfo.isWildcard;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type ProjectSettings = import('@vercel/build-utils').ProjectSettings;
|
||||
|
||||
export type Primitive =
|
||||
| bigint
|
||||
| boolean
|
||||
@@ -239,16 +241,6 @@ export interface ProjectEnvVariable {
|
||||
gitBranch?: string;
|
||||
}
|
||||
|
||||
export interface ProjectSettings {
|
||||
framework?: string | null;
|
||||
devCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
autoExposeSystemEnvs?: boolean;
|
||||
directoryListing?: boolean;
|
||||
}
|
||||
|
||||
export interface Project extends ProjectSettings {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -260,8 +252,6 @@ export interface Project extends ProjectSettings {
|
||||
framework?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
latestDeployments?: Partial<Deployment>[];
|
||||
autoExposeSystemEnvs?: boolean;
|
||||
sourceFilesOutsideRootDirectory: boolean;
|
||||
}
|
||||
|
||||
export interface Org {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,14 +6,14 @@ import getLocalConfigPath from './local-path';
|
||||
|
||||
export default async function readConfig(dir: string) {
|
||||
const pkgFilePath = getLocalConfigPath(join(process.cwd(), dir));
|
||||
const result = await readJSONFile(pkgFilePath);
|
||||
const result = await readJSONFile<VercelConfig>(pkgFilePath);
|
||||
|
||||
if (result instanceof CantParseJSONFile) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return result as VercelConfig;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -52,6 +52,7 @@ export default async function processDeployment({
|
||||
isSettingUpProject: boolean;
|
||||
skipAutoDetectionConfirmation?: boolean;
|
||||
cwd?: string;
|
||||
rootDirectory?: string;
|
||||
}) {
|
||||
let {
|
||||
now,
|
||||
@@ -64,6 +65,7 @@ export default async function processDeployment({
|
||||
nowConfig,
|
||||
quiet,
|
||||
prebuilt,
|
||||
rootDirectory,
|
||||
} = args;
|
||||
|
||||
const { debug } = output;
|
||||
@@ -86,6 +88,7 @@ export default async function processDeployment({
|
||||
force,
|
||||
withCache,
|
||||
prebuilt,
|
||||
rootDirectory,
|
||||
skipAutoDetectionConfirmation,
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
detectApiExtensions,
|
||||
spawnCommand,
|
||||
isOfficialRuntime,
|
||||
detectFileSystemAPI,
|
||||
} from '@vercel/build-utils';
|
||||
import frameworkList from '@vercel/frameworks';
|
||||
|
||||
@@ -599,6 +600,32 @@ export default class DevServer {
|
||||
);
|
||||
}
|
||||
|
||||
const { reason, metadata } = await detectFileSystemAPI({
|
||||
files,
|
||||
builders: builders || [],
|
||||
projectSettings: projectSettings || this.projectSettings || {},
|
||||
vercelConfig,
|
||||
pkg,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
|
||||
if (reason) {
|
||||
if (metadata.hasMiddleware) {
|
||||
this.output.error(
|
||||
`Detected middleware usage which requires the latest API. ${reason}`
|
||||
);
|
||||
await this.exit();
|
||||
} else if (metadata.plugins.length > 0) {
|
||||
this.output.error(
|
||||
`Detected CLI plugins which requires the latest API. ${reason}`
|
||||
);
|
||||
await this.exit();
|
||||
} else {
|
||||
this.output.warn(`Unable to use latest API. ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (builders) {
|
||||
if (this.devCommand) {
|
||||
builders = builders.filter(filterFrontendBuilds);
|
||||
@@ -968,7 +995,7 @@ export default class DevServer {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
const target = `http://localhost:${this.devProcessPort}`;
|
||||
const target = `http://127.0.0.1:${this.devProcessPort}`;
|
||||
this.output.debug(`Detected "upgrade" event, proxying to ${target}`);
|
||||
this.proxy.ws(req, socket, head, { target });
|
||||
});
|
||||
@@ -1358,13 +1385,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 };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1656,7 +1690,7 @@ export default class DevServer {
|
||||
if (!match) {
|
||||
// If the dev command is started, then proxy to it
|
||||
if (this.devProcessPort) {
|
||||
const upstream = `http://localhost:${this.devProcessPort}`;
|
||||
const upstream = `http://127.0.0.1:${this.devProcessPort}`;
|
||||
debug(`Proxying to frontend dev server: ${upstream}`);
|
||||
|
||||
// Add the Vercel platform proxy request headers
|
||||
@@ -1803,7 +1837,7 @@ export default class DevServer {
|
||||
return proxyPass(
|
||||
req,
|
||||
res,
|
||||
`http://localhost:${port}`,
|
||||
`http://127.0.0.1:${port}`,
|
||||
this,
|
||||
requestId,
|
||||
false
|
||||
@@ -1840,7 +1874,7 @@ export default class DevServer {
|
||||
return proxyPass(
|
||||
req,
|
||||
res,
|
||||
`http://localhost:${this.devProcessPort}`,
|
||||
`http://127.0.0.1:${this.devProcessPort}`,
|
||||
this,
|
||||
requestId,
|
||||
false
|
||||
@@ -2154,7 +2188,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;
|
||||
});
|
||||
|
||||
|
||||
@@ -39,12 +39,12 @@ export default async function getConfig(
|
||||
output.debug(
|
||||
`Found config in provided --local-config path ${localFilePath}`
|
||||
);
|
||||
const localConfig = await readJSONFile(localFilePath);
|
||||
const localConfig = await readJSONFile<VercelConfig>(localFilePath);
|
||||
if (localConfig instanceof CantParseJSONFile) {
|
||||
return localConfig;
|
||||
}
|
||||
if (localConfig !== null) {
|
||||
config = localConfig as VercelConfig;
|
||||
config = localConfig;
|
||||
config[fileNameSymbol] = configFile;
|
||||
return config;
|
||||
}
|
||||
@@ -54,8 +54,8 @@ export default async function getConfig(
|
||||
const vercelFilePath = path.resolve(localPath, 'vercel.json');
|
||||
const nowFilePath = path.resolve(localPath, 'now.json');
|
||||
const [vercelConfig, nowConfig] = await Promise.all([
|
||||
readJSONFile(vercelFilePath),
|
||||
readJSONFile(nowFilePath),
|
||||
readJSONFile<VercelConfig>(vercelFilePath),
|
||||
readJSONFile<VercelConfig>(nowFilePath),
|
||||
]);
|
||||
if (vercelConfig instanceof CantParseJSONFile) {
|
||||
return vercelConfig;
|
||||
@@ -68,13 +68,13 @@ export default async function getConfig(
|
||||
}
|
||||
if (vercelConfig !== null) {
|
||||
output.debug(`Found config in file "${vercelFilePath}"`);
|
||||
config = vercelConfig as VercelConfig;
|
||||
config = vercelConfig;
|
||||
config[fileNameSymbol] = 'vercel.json';
|
||||
return config;
|
||||
}
|
||||
if (nowConfig !== null) {
|
||||
output.debug(`Found config in file "${nowFilePath}"`);
|
||||
config = nowConfig as VercelConfig;
|
||||
config = nowConfig;
|
||||
config[fileNameSymbol] = 'now.json';
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ import { VercelConfig } from '@vercel/client';
|
||||
export interface GetProjectNameOptions {
|
||||
argv: { '--name'?: string };
|
||||
nowConfig?: VercelConfig;
|
||||
isFile?: boolean;
|
||||
paths?: string[];
|
||||
}
|
||||
|
||||
export default function getProjectName({
|
||||
argv,
|
||||
nowConfig = {},
|
||||
isFile = false,
|
||||
paths = [],
|
||||
}: GetProjectNameOptions) {
|
||||
const nameCli = argv['--name'];
|
||||
@@ -24,10 +22,6 @@ export default function getProjectName({
|
||||
return nowConfig.name;
|
||||
}
|
||||
|
||||
if (isFile || paths.length > 1) {
|
||||
return 'files';
|
||||
}
|
||||
|
||||
// Otherwise, use the name of the directory
|
||||
return basename(paths[0] || '');
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -30,13 +30,13 @@ export interface NowOptions {
|
||||
export interface CreateOptions {
|
||||
// Legacy
|
||||
nowConfig?: VercelConfig;
|
||||
isFile?: boolean;
|
||||
|
||||
// Latest
|
||||
name: string;
|
||||
project?: string;
|
||||
wantsPublic: boolean;
|
||||
prebuilt?: boolean;
|
||||
rootDirectory?: string;
|
||||
meta: Dictionary<string>;
|
||||
regions?: string[];
|
||||
quiet?: boolean;
|
||||
@@ -113,6 +113,7 @@ export default class Now extends EventEmitter {
|
||||
name,
|
||||
project,
|
||||
prebuilt = false,
|
||||
rootDirectory,
|
||||
wantsPublic,
|
||||
meta,
|
||||
regions,
|
||||
@@ -168,6 +169,7 @@ export default class Now extends EventEmitter {
|
||||
skipAutoDetectionConfirmation,
|
||||
cwd,
|
||||
prebuilt,
|
||||
rootDirectory,
|
||||
});
|
||||
|
||||
if (deployment && deployment.warnings) {
|
||||
|
||||
@@ -11,7 +11,7 @@ export default async function inputProject(
|
||||
client: Client,
|
||||
org: Org,
|
||||
detectedProjectName: string,
|
||||
autoConfirm: boolean
|
||||
autoConfirm = false
|
||||
): Promise<Project | string> {
|
||||
const { output } = client;
|
||||
const slugifiedName = slugify(detectedProjectName);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { validateRootDirectory } from '../validate-paths';
|
||||
export async function inputRootDirectory(
|
||||
cwd: string,
|
||||
output: Output,
|
||||
autoConfirm: boolean
|
||||
autoConfirm = false
|
||||
) {
|
||||
if (autoConfirm) {
|
||||
return null;
|
||||
|
||||
@@ -158,7 +158,6 @@ export default async function setupAndLink(
|
||||
withCache: undefined,
|
||||
quiet,
|
||||
wantsPublic: localConfig?.public || false,
|
||||
isFile,
|
||||
nowConfig: localConfig,
|
||||
regions: undefined,
|
||||
meta: {},
|
||||
@@ -179,7 +178,7 @@ export default async function setupAndLink(
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
!isFile,
|
||||
true,
|
||||
path
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ProjectLinkAndSettings = ProjectLink & {
|
||||
buildCommand: Project['buildCommand'];
|
||||
devCommand: Project['devCommand'];
|
||||
outputDirectory: Project['outputDirectory'];
|
||||
directoryListing: Project['directoryListing'];
|
||||
rootDirectory: Project['rootDirectory'];
|
||||
framework: Project['framework'];
|
||||
};
|
||||
@@ -29,6 +30,7 @@ export async function writeProjectSettings(
|
||||
settings: {
|
||||
buildCommand: project.buildCommand,
|
||||
devCommand: project.devCommand,
|
||||
outputDirectory: project.outputDirectory,
|
||||
directoryListing: project.directoryListing,
|
||||
rootDirectory: project.rootDirectory,
|
||||
framework: project.framework,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs-extra';
|
||||
import { CantParseJSONFile } from './errors-ts';
|
||||
|
||||
export default async function readJSONFile(
|
||||
export default async function readJSONFile<T>(
|
||||
file: string
|
||||
): Promise<Object | null | CantParseJSONFile> {
|
||||
): Promise<T | null | CantParseJSONFile> {
|
||||
const content = await readFileSafe(file);
|
||||
if (content === null) {
|
||||
return content;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Output } from './output';
|
||||
import chalk from 'chalk';
|
||||
import { homedir } from 'os';
|
||||
import confirm from './input/confirm';
|
||||
import { prependEmoji, emoji } from './emoji';
|
||||
import toHumanPath from './humanize-path';
|
||||
|
||||
const stat = promisify(lstatRaw);
|
||||
@@ -54,10 +53,7 @@ export async function validateRootDirectory(
|
||||
export default async function validatePaths(
|
||||
output: Output,
|
||||
paths: string[]
|
||||
): Promise<
|
||||
| { valid: true; path: string; isFile: boolean }
|
||||
| { valid: false; exitCode: number }
|
||||
> {
|
||||
): Promise<{ valid: true; path: string } | { valid: false; exitCode: number }> {
|
||||
// can't deploy more than 1 path
|
||||
if (paths.length > 1) {
|
||||
output.print(`${chalk.red('Error!')} Can't deploy more than one path.\n`);
|
||||
@@ -78,14 +74,12 @@ export default async function validatePaths(
|
||||
return { valid: false, exitCode: 1 };
|
||||
}
|
||||
|
||||
const isFile = pathStat && !pathStat.isDirectory();
|
||||
if (isFile) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
'Deploying files with Vercel is deprecated (https://vercel.link/faq-deploy-file)',
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
if (!pathStat.isDirectory()) {
|
||||
output.prettyError({
|
||||
message: 'Support for single file deployments has been removed.',
|
||||
link: 'https://vercel.link/no-single-file-deployments',
|
||||
});
|
||||
return { valid: false, exitCode: 1 };
|
||||
}
|
||||
|
||||
// ask confirmation if the directory is home
|
||||
@@ -101,5 +95,5 @@ export default async function validatePaths(
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, path, isFile };
|
||||
return { valid: true, path };
|
||||
}
|
||||
|
||||
60
packages/cli/test/commands/deploy.test.ts
Normal file
60
packages/cli/test/commands/deploy.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { join } from 'path';
|
||||
import { fileNameSymbol } from '@vercel/client';
|
||||
import { client } from '../mocks/client';
|
||||
import deploy from '../../src/commands/deploy';
|
||||
|
||||
describe('deploy', () => {
|
||||
it('should reject deploying a single file', async () => {
|
||||
client.setArgv('deploy', __filename);
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
`Error! Support for single file deployments has been removed.\nLearn More: https://vercel.link/no-single-file-deployments\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject deploying multiple files', async () => {
|
||||
client.setArgv('deploy', __filename, join(__dirname, 'inspect.test.ts'));
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
`Error! Can't deploy more than one path.\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject deploying a directory that does not exist', async () => {
|
||||
client.setArgv('deploy', 'does-not-exists');
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
`Error! The specified file or directory "does-not-exists" does not exist.\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject deploying "version: 1"', async () => {
|
||||
client.setArgv('deploy');
|
||||
client.localConfig = {
|
||||
[fileNameSymbol]: 'vercel.json',
|
||||
version: 1,
|
||||
};
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
'Error! The value of the `version` property within vercel.json can only be `2`.\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject deploying "version: {}"', async () => {
|
||||
client.setArgv('deploy');
|
||||
client.localConfig = {
|
||||
[fileNameSymbol]: 'vercel.json',
|
||||
// @ts-ignore
|
||||
version: {},
|
||||
};
|
||||
const exitCode = await deploy(client);
|
||||
expect(exitCode).toEqual(1);
|
||||
expect(client.outputBuffer).toEqual(
|
||||
'Error! The `version` property inside your vercel.json file must be a number.\n'
|
||||
);
|
||||
});
|
||||
});
|
||||
3
packages/cli/test/fixtures/unit/edge-middleware-invalid-response/_middleware.js
vendored
Normal file
3
packages/cli/test/fixtures/unit/edge-middleware-invalid-response/_middleware.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function () {
|
||||
return 'freecandy';
|
||||
}
|
||||
6
packages/cli/test/fixtures/unit/edge-middleware-strict/_middleware.ts
vendored
Normal file
6
packages/cli/test/fixtures/unit/edge-middleware-strict/_middleware.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function (req) {
|
||||
const isStrict = (function () {
|
||||
return !this;
|
||||
})();
|
||||
return new Response('is strict mode? ' + (isStrict ? 'yes' : 'no'));
|
||||
}
|
||||
@@ -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"}]}',
|
||||
|
||||
449
packages/cli/test/integration.js
vendored
449
packages/cli/test/integration.js
vendored
@@ -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('What’s your project’s 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);
|
||||
@@ -1854,59 +1913,6 @@ test('create a production deployment', async t => {
|
||||
t.is(deployment.target, 'production', JSON.stringify(deployment, null, 2));
|
||||
});
|
||||
|
||||
test('deploying a file should not show prompts and display deprecation', async t => {
|
||||
const file = fixture('static-single-file/first.png');
|
||||
|
||||
const output = await execute([file], {
|
||||
reject: false,
|
||||
});
|
||||
|
||||
const { stdout, stderr, exitCode } = output;
|
||||
|
||||
// Ensure the exit code is right
|
||||
t.is(exitCode, 0, formatOutput(output));
|
||||
t.true(stderr.includes('Deploying files with Vercel is deprecated'));
|
||||
|
||||
// Ensure `.vercel` was not created
|
||||
t.is(
|
||||
await exists(path.join(path.dirname(file), '.vercel')),
|
||||
false,
|
||||
'.vercel should not exists'
|
||||
);
|
||||
|
||||
// Test if the output is really a URL
|
||||
const { href, host } = new URL(stdout);
|
||||
t.is(host.split('-')[0], 'files');
|
||||
|
||||
// Send a test request to the deployment
|
||||
const response = await fetch(href);
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
t.is(contentType, 'image/png');
|
||||
t.deepEqual(await readFile(file), await response.buffer());
|
||||
});
|
||||
|
||||
test('deploying more than 1 path should fail', async t => {
|
||||
const file1 = fixture('static-multiple-files/first.png');
|
||||
const file2 = fixture('static-multiple-files/second.png');
|
||||
|
||||
const { stdout, stderr, exitCode } = await execa(
|
||||
binaryPath,
|
||||
[file1, file2, '--public', '--name', session, ...defaultArgs, '--confirm'],
|
||||
{
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(exitCode);
|
||||
|
||||
// Ensure the exit code is right
|
||||
t.is(exitCode, 1);
|
||||
t.true(stderr.trim().endsWith(`Can't deploy more than one path.`));
|
||||
});
|
||||
|
||||
test('use build-env', async t => {
|
||||
const directory = fixture('build-env');
|
||||
|
||||
@@ -2227,13 +2233,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 +2630,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 +2639,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 +2769,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('What’s your project’s 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 +3338,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('What’s your project’s 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 +3400,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 +3437,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('What’s your project’s 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 +3540,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('What’s your project’s 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
|
||||
|
||||
@@ -374,4 +374,24 @@ 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'
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
it(
|
||||
'should run middleware in strict mode',
|
||||
testFixture('edge-middleware-strict', async server => {
|
||||
const response = await fetch(`${server.address}/index.html`);
|
||||
const body = await response.text();
|
||||
expect(body).toStrictEqual('is strict mode? yes');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('getProjectName', () => {
|
||||
expect(project).toEqual('abc');
|
||||
});
|
||||
|
||||
it('should work with now.json', () => {
|
||||
it('should work with `vercel.json` config', () => {
|
||||
const project = getProjectName({
|
||||
argv: {},
|
||||
nowConfig: { name: 'abc' },
|
||||
@@ -18,24 +18,6 @@ describe('getProjectName', () => {
|
||||
expect(project).toEqual('abc');
|
||||
});
|
||||
|
||||
it('should work with a file', () => {
|
||||
const project = getProjectName({
|
||||
argv: {},
|
||||
nowConfig: {},
|
||||
isFile: true,
|
||||
});
|
||||
expect(project).toEqual('files');
|
||||
});
|
||||
|
||||
it('should work with a multiple files', () => {
|
||||
const project = getProjectName({
|
||||
argv: {},
|
||||
nowConfig: {},
|
||||
paths: ['/tmp/aa/abc.png', '/tmp/aa/bbc.png'],
|
||||
});
|
||||
expect(project).toEqual('files');
|
||||
});
|
||||
|
||||
it('should work with a directory', () => {
|
||||
const project = getProjectName({
|
||||
argv: {},
|
||||
|
||||
@@ -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'}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
2
packages/client/.gitignore
vendored
2
packages/client/.gitignore
vendored
@@ -6,3 +6,5 @@ node_modules
|
||||
!tests/fixtures/nowignore/node_modules
|
||||
!tests/fixtures/vercelignore-allow-nodemodules/node_modules
|
||||
!tests/fixtures/vercelignore-allow-nodemodules/sub/node_modules
|
||||
!tests/fixtures/file-system-api/.output
|
||||
!tests/fixtures/file-system-api-root-directory/**/.output
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "10.2.3-canary.18",
|
||||
"version": "10.2.3-canary.46",
|
||||
"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.43",
|
||||
"@zeit/fetch": "5.2.0",
|
||||
"async-retry": "1.2.3",
|
||||
"async-sema": "3.0.0",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { lstatSync } from 'fs-extra';
|
||||
|
||||
import { relative, isAbsolute } from 'path';
|
||||
import hashes, { mapToObject } from './utils/hashes';
|
||||
import { hashes, mapToObject, resolveNftJsonFiles } from './utils/hashes';
|
||||
import { upload } from './upload';
|
||||
import { buildFileTree, createDebug, parseVercelConfig } from './utils';
|
||||
import { DeploymentError } from './errors';
|
||||
import {
|
||||
NowConfig,
|
||||
VercelConfig,
|
||||
VercelClientOptions,
|
||||
DeploymentOptions,
|
||||
DeploymentEventType,
|
||||
@@ -16,7 +16,7 @@ export default function buildCreateDeployment() {
|
||||
return async function* createDeployment(
|
||||
clientOptions: VercelClientOptions,
|
||||
deploymentOptions: DeploymentOptions = {},
|
||||
nowConfig: NowConfig = {}
|
||||
nowConfig: VercelConfig = {}
|
||||
): AsyncIterableIterator<{ type: DeploymentEventType; payload: any }> {
|
||||
const { path } = clientOptions;
|
||||
|
||||
@@ -74,12 +74,7 @@ export default function buildCreateDeployment() {
|
||||
debug(`Provided 'path' is a single file`);
|
||||
}
|
||||
|
||||
let { fileList } = await buildFileTree(
|
||||
path,
|
||||
clientOptions.isDirectory,
|
||||
debug,
|
||||
clientOptions.prebuilt
|
||||
);
|
||||
let { fileList } = await buildFileTree(path, clientOptions, debug);
|
||||
|
||||
let configPath: string | undefined;
|
||||
if (!nowConfig) {
|
||||
@@ -114,7 +109,11 @@ export default function buildCreateDeployment() {
|
||||
};
|
||||
}
|
||||
|
||||
const files = await hashes(fileList);
|
||||
const hashedFileMap = await hashes(fileList);
|
||||
const nftFileList = clientOptions.prebuilt
|
||||
? await resolveNftJsonFiles(hashedFileMap)
|
||||
: [];
|
||||
const files = await hashes(nftFileList, hashedFileMap);
|
||||
|
||||
debug(`Yielding a 'hashes-calculated' event with ${files.size} hashes`);
|
||||
yield { type: 'hashes-calculated', payload: mapToObject(files) };
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Builder, BuilderFunctions } from '@vercel/build-utils';
|
||||
import {
|
||||
Builder,
|
||||
BuilderFunctions,
|
||||
ProjectSettings,
|
||||
} from '@vercel/build-utils';
|
||||
import { Header, Route, Redirect, Rewrite } from '@vercel/routing-utils';
|
||||
|
||||
export { DeploymentEventType } from './utils';
|
||||
@@ -15,6 +19,7 @@ export interface VercelClientOptions {
|
||||
apiUrl?: string;
|
||||
force?: boolean;
|
||||
prebuilt?: boolean;
|
||||
rootDirectory?: string;
|
||||
withCache?: boolean;
|
||||
userAgent?: string;
|
||||
defaultName?: string;
|
||||
@@ -123,12 +128,7 @@ export interface VercelConfig {
|
||||
scope?: string;
|
||||
alias?: string | string[];
|
||||
regions?: string[];
|
||||
projectSettings?: {
|
||||
devCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
framework?: string | null;
|
||||
};
|
||||
projectSettings?: ProjectSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,9 +154,5 @@ export interface DeploymentOptions {
|
||||
name?: string;
|
||||
public?: boolean;
|
||||
meta?: Dictionary<string>;
|
||||
projectSettings?: {
|
||||
devCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
};
|
||||
projectSettings?: ProjectSettings;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import { Sema } from 'async-sema';
|
||||
import { join, dirname } from 'path';
|
||||
|
||||
export interface DeploymentFile {
|
||||
names: string[];
|
||||
@@ -15,9 +16,7 @@ export interface DeploymentFile {
|
||||
* @return {String} hex digest
|
||||
*/
|
||||
function hash(buf: Buffer): string {
|
||||
return createHash('sha1')
|
||||
.update(buf)
|
||||
.digest('hex');
|
||||
return createHash('sha1').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,34 +38,68 @@ export const mapToObject = (
|
||||
/**
|
||||
* Computes hashes for the contents of each file given.
|
||||
*
|
||||
* @param {Array} of {String} full paths
|
||||
* @return {Map}
|
||||
* @param files - absolute file paths
|
||||
* @param map - optional map of files to append
|
||||
* @return Map of hash digest to file object
|
||||
*/
|
||||
async function hashes(files: string[]): Promise<Map<string, DeploymentFile>> {
|
||||
const map = new Map<string, DeploymentFile>();
|
||||
export async function hashes(
|
||||
files: string[],
|
||||
map = new Map<string, DeploymentFile>()
|
||||
): Promise<Map<string, DeploymentFile>> {
|
||||
const semaphore = new Sema(100);
|
||||
|
||||
await Promise.all(
|
||||
files.map(
|
||||
async (name: string): Promise<void> => {
|
||||
await semaphore.acquire();
|
||||
const data = await fs.readFile(name);
|
||||
const { mode } = await fs.stat(name);
|
||||
files.map(async (name: string): Promise<void> => {
|
||||
await semaphore.acquire();
|
||||
const data = await fs.readFile(name);
|
||||
const { mode } = await fs.stat(name);
|
||||
|
||||
const h = hash(data);
|
||||
const entry = map.get(h);
|
||||
const h = hash(data);
|
||||
const entry = map.get(h);
|
||||
|
||||
if (entry) {
|
||||
entry.names.push(name);
|
||||
} else {
|
||||
map.set(h, { names: [name], data, mode });
|
||||
}
|
||||
|
||||
semaphore.release();
|
||||
if (entry) {
|
||||
const names = new Set(entry.names);
|
||||
names.add(name);
|
||||
entry.names = [...names];
|
||||
} else {
|
||||
map.set(h, { names: [name], data, mode });
|
||||
}
|
||||
)
|
||||
|
||||
semaphore.release();
|
||||
})
|
||||
);
|
||||
return map;
|
||||
}
|
||||
|
||||
export default hashes;
|
||||
export async function resolveNftJsonFiles(
|
||||
hashedFiles: Map<string, DeploymentFile>
|
||||
): Promise<string[]> {
|
||||
const semaphore = new Sema(100);
|
||||
const existingFiles = Array.from(hashedFiles.values());
|
||||
const resolvedFiles = new Set<string>();
|
||||
|
||||
await Promise.all(
|
||||
existingFiles.map(async file => {
|
||||
await semaphore.acquire();
|
||||
const fsPath = file.names[0];
|
||||
if (fsPath.endsWith('.nft.json')) {
|
||||
const json = file.data.toString('utf8');
|
||||
const { version, files } = JSON.parse(json) as {
|
||||
version: number;
|
||||
files: string[] | { input: string; output: string }[];
|
||||
};
|
||||
if (version === 1 || version === 2) {
|
||||
for (let f of files) {
|
||||
const relPath = typeof f === 'string' ? f : f.input;
|
||||
resolvedFiles.add(join(dirname(fsPath), relPath));
|
||||
}
|
||||
} else {
|
||||
console.error(`Invalid nft.json version: ${version}`);
|
||||
}
|
||||
}
|
||||
semaphore.release();
|
||||
})
|
||||
);
|
||||
|
||||
return Array.from(resolvedFiles);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DeploymentFile } from './hashes';
|
||||
import { FetchOptions } from '@zeit/fetch';
|
||||
import { nodeFetch, zeitFetch } from './fetch';
|
||||
import { join, sep, relative } from 'path';
|
||||
import { join, sep, relative, posix } from 'path';
|
||||
import { URL } from 'url';
|
||||
import ignore from 'ignore';
|
||||
type Ignore = ReturnType<typeof ignore>;
|
||||
@@ -81,13 +81,16 @@ const maybeRead = async function <T>(path: string, default_: T) {
|
||||
|
||||
export async function buildFileTree(
|
||||
path: string | string[],
|
||||
isDirectory: boolean,
|
||||
debug: Debug,
|
||||
prebuilt?: boolean
|
||||
{
|
||||
isDirectory,
|
||||
prebuilt,
|
||||
rootDirectory,
|
||||
}: Pick<VercelClientOptions, 'isDirectory' | 'prebuilt' | 'rootDirectory'>,
|
||||
debug: Debug
|
||||
): Promise<{ fileList: string[]; ignoreList: string[] }> {
|
||||
const ignoreList: string[] = [];
|
||||
let fileList: string[];
|
||||
let { ig, ignores } = await getVercelIgnore(path, prebuilt);
|
||||
let { ig, ignores } = await getVercelIgnore(path, prebuilt, rootDirectory);
|
||||
|
||||
debug(`Found ${ignores.length} rules in .vercelignore`);
|
||||
debug('Building file tree...');
|
||||
@@ -119,37 +122,50 @@ export async function buildFileTree(
|
||||
|
||||
export async function getVercelIgnore(
|
||||
cwd: string | string[],
|
||||
prebuilt?: boolean
|
||||
prebuilt?: boolean,
|
||||
rootDirectory?: string
|
||||
): Promise<{ ig: Ignore; ignores: string[] }> {
|
||||
const ignores: string[] = prebuilt
|
||||
? ['*', '!.output', '!.output/**']
|
||||
: [
|
||||
'.hg',
|
||||
'.git',
|
||||
'.gitmodules',
|
||||
'.svn',
|
||||
'.cache',
|
||||
'.next',
|
||||
'.now',
|
||||
'.vercel',
|
||||
'.npmignore',
|
||||
'.dockerignore',
|
||||
'.gitignore',
|
||||
'.*.swp',
|
||||
'.DS_Store',
|
||||
'.wafpicke-*',
|
||||
'.lock-wscript',
|
||||
'.env.local',
|
||||
'.env.*.local',
|
||||
'.venv',
|
||||
'npm-debug.log',
|
||||
'config.gypi',
|
||||
'node_modules',
|
||||
'__pycache__',
|
||||
'venv',
|
||||
'CVS',
|
||||
'.output',
|
||||
];
|
||||
let ignores: string[] = [];
|
||||
|
||||
const outputDir = posix.join(rootDirectory || '', '.output');
|
||||
|
||||
if (prebuilt) {
|
||||
ignores.push('*');
|
||||
const parts = outputDir.split('/');
|
||||
parts.forEach((_, i) => {
|
||||
const level = parts.slice(0, i + 1).join('/');
|
||||
ignores.push(`!${level}`);
|
||||
});
|
||||
ignores.push(`!${outputDir}/**`);
|
||||
} else {
|
||||
ignores = [
|
||||
'.hg',
|
||||
'.git',
|
||||
'.gitmodules',
|
||||
'.svn',
|
||||
'.cache',
|
||||
'.next',
|
||||
'.now',
|
||||
'.vercel',
|
||||
'.npmignore',
|
||||
'.dockerignore',
|
||||
'.gitignore',
|
||||
'.*.swp',
|
||||
'.DS_Store',
|
||||
'.wafpicke-*',
|
||||
'.lock-wscript',
|
||||
'.env.local',
|
||||
'.env.*.local',
|
||||
'.venv',
|
||||
'npm-debug.log',
|
||||
'config.gypi',
|
||||
'node_modules',
|
||||
'__pycache__',
|
||||
'venv',
|
||||
'CVS',
|
||||
`.output`,
|
||||
];
|
||||
}
|
||||
const cwds = Array.isArray(cwd) ? cwd : [cwd];
|
||||
|
||||
const files = await Promise.all(
|
||||
@@ -250,39 +266,31 @@ export const prepareFiles = (
|
||||
files: Map<string, DeploymentFile>,
|
||||
clientOptions: VercelClientOptions
|
||||
): PreparedFile[] => {
|
||||
const preparedFiles = [...files.keys()].reduce(
|
||||
(acc: PreparedFile[], sha: string): PreparedFile[] => {
|
||||
const next = [...acc];
|
||||
const preparedFiles: PreparedFile[] = [];
|
||||
for (const [sha, file] of files) {
|
||||
for (const name of file.names) {
|
||||
let fileName: string;
|
||||
|
||||
const file = files.get(sha) as DeploymentFile;
|
||||
|
||||
for (const name of file.names) {
|
||||
let fileName: string;
|
||||
|
||||
if (clientOptions.isDirectory) {
|
||||
// Directory
|
||||
fileName =
|
||||
typeof clientOptions.path === 'string'
|
||||
? relative(clientOptions.path, name)
|
||||
: name;
|
||||
} else {
|
||||
// Array of files or single file
|
||||
const segments = name.split(sep);
|
||||
fileName = segments[segments.length - 1];
|
||||
}
|
||||
|
||||
next.push({
|
||||
file: isWin ? fileName.replace(/\\/g, '/') : fileName,
|
||||
size: file.data.byteLength || file.data.length,
|
||||
mode: file.mode,
|
||||
sha,
|
||||
});
|
||||
if (clientOptions.isDirectory) {
|
||||
// Directory
|
||||
fileName =
|
||||
typeof clientOptions.path === 'string'
|
||||
? relative(clientOptions.path, name)
|
||||
: name;
|
||||
} else {
|
||||
// Array of files or single file
|
||||
const segments = name.split(sep);
|
||||
fileName = segments[segments.length - 1];
|
||||
}
|
||||
|
||||
return next;
|
||||
},
|
||||
[]
|
||||
);
|
||||
preparedFiles.push({
|
||||
file: isWin ? fileName.replace(/\\/g, '/') : fileName,
|
||||
size: file.data.byteLength || file.data.length,
|
||||
mode: file.mode,
|
||||
sha,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return preparedFiles;
|
||||
};
|
||||
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/foo.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/foo.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
foo
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/root/.output/baz.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/root/.output/baz.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
baz
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/root/.output/sub/qux.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/root/.output/sub/qux.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
qux
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user