mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 04:22:13 +00:00
Compare commits
164 Commits
@vercel/ro
...
vercel-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
ed1dacd276 | ||
|
|
144e890bfa | ||
|
|
af097c2c06 | ||
|
|
873a582986 | ||
|
|
986b4c0b1a | ||
|
|
14071819ac | ||
|
|
2a8588a0c5 | ||
|
|
0f7e89f76c | ||
|
|
e68ed33a88 | ||
|
|
d3e98cdb73 | ||
|
|
bf4e77110f | ||
|
|
5b5197d2c5 | ||
|
|
a6ccf6c180 | ||
|
|
8d848ebe8b | ||
|
|
6ef2c16d63 | ||
|
|
6c71ceaaeb | ||
|
|
1dcb6dfc6f | ||
|
|
4fd24575e5 | ||
|
|
8714f1905e | ||
|
|
2e69f2513d | ||
|
|
979e4b674a | ||
|
|
07fa47bcfb | ||
|
|
307c4fc377 | ||
|
|
44868d79b6 | ||
|
|
df9a4afa5c | ||
|
|
8a6869bae2 | ||
|
|
a3fc3c1ca7 | ||
|
|
44037c58be | ||
|
|
1a9419b690 | ||
|
|
93d0e5966c | ||
|
|
306f3a1312 | ||
|
|
9c67e8115e | ||
|
|
b890ac1e44 | ||
|
|
28e71ff109 | ||
|
|
2bf060c708 | ||
|
|
c8ef1d71d1 | ||
|
|
3d2efc7dcd | ||
|
|
cd7185a872 | ||
|
|
981a76fbe2 | ||
|
|
c35c05446b | ||
|
|
b88e65c4ad | ||
|
|
9e16ce750b | ||
|
|
2105d31730 | ||
|
|
3d7e01ebf0 | ||
|
|
dccacc4ca0 | ||
|
|
be5c0da521 | ||
|
|
f985d953ed | ||
|
|
b8f8289afe | ||
|
|
e01a1cebfd | ||
|
|
2515d522a3 | ||
|
|
32e935d632 | ||
|
|
e6818dd3f9 | ||
|
|
e510415a66 | ||
|
|
1be75712e0 | ||
|
|
f682aefc9d | ||
|
|
cd485c1866 | ||
|
|
f7de7227b4 | ||
|
|
abea217177 | ||
|
|
4e52f8532b | ||
|
|
702cb9e29c | ||
|
|
d3d5555d79 | ||
|
|
2fd3fc73e5 | ||
|
|
de0b13a46e | ||
|
|
d0fe85db92 | ||
|
|
bfbd927320 | ||
|
|
90bacf88b8 | ||
|
|
07c369c542 | ||
|
|
a2e4186ccb | ||
|
|
6e1d708e3f | ||
|
|
38503103c3 | ||
|
|
e8fec4b69c | ||
|
|
b3ffcdf80d | ||
|
|
43c1a93c1d | ||
|
|
5b118fd4e6 | ||
|
|
8916b674af | ||
|
|
1807f83c69 | ||
|
|
74e8ec7c64 | ||
|
|
2644e3127b | ||
|
|
d77ac04b0c | ||
|
|
0ef9c8df4d | ||
|
|
dfc4c98820 | ||
|
|
0e51884725 | ||
|
|
1b264fe60e | ||
|
|
f18bca9718 | ||
|
|
c23dc73f41 | ||
|
|
273718e0b7 | ||
|
|
230b88bf9b | ||
|
|
676a3d2568 | ||
|
|
f221f041d0 | ||
|
|
aca42b2aac | ||
|
|
cf11a8efb5 | ||
|
|
be09349daf | ||
|
|
a01372bcbb | ||
|
|
b941715d7b | ||
|
|
ee9a8a0415 | ||
|
|
2ad27eefb0 | ||
|
|
578fe8a930 | ||
|
|
04ea3bb88d | ||
|
|
46116022b7 | ||
|
|
f80539df82 | ||
|
|
daf06307b4 | ||
|
|
0bd028cd84 | ||
|
|
1c48030e1e | ||
|
|
1dc05428d7 | ||
|
|
288dca045c | ||
|
|
8c5bc04fde | ||
|
|
a07e6fc103 | ||
|
|
9af3054d41 | ||
|
|
9fb254e14a | ||
|
|
3616bdf17a | ||
|
|
52a89fd4b7 | ||
|
|
d4db6635f1 | ||
|
|
f1009a80cd | ||
|
|
2756d1e323 | ||
|
|
5b61b16bd1 | ||
|
|
41c61f8f8b | ||
|
|
6c52e1fad7 | ||
|
|
d2e82fdc3a | ||
|
|
a60b1b225b | ||
|
|
18bec983ae | ||
|
|
e6fb2ffe05 | ||
|
|
0533cfd566 | ||
|
|
3db8618885 | ||
|
|
4722ea5ad6 | ||
|
|
c7bd6f3266 |
@@ -1,11 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
examples
|
||||
packages/build-utils/test/fixtures
|
||||
packages/*/test/fixtures
|
||||
packages/cli/@types
|
||||
packages/cli/download
|
||||
packages/cli/dist
|
||||
packages/cli/test/fixtures
|
||||
packages/cli/test/dev/fixtures
|
||||
packages/cli/bin
|
||||
packages/cli/link
|
||||
@@ -13,6 +12,6 @@ packages/cli/src/util/dev/templates/*.ts
|
||||
packages/client/tests/fixtures
|
||||
packages/client/lib
|
||||
packages/node/src/bridge.ts
|
||||
packages/node/test/fixtures
|
||||
packages/node-bridge/bridge.js
|
||||
packages/node-bridge/launcher.js
|
||||
packages/middleware/src/entries.js
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Bug Report
|
||||
url: https://vercel.com/support/request
|
||||
|
||||
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/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -29,6 +29,6 @@ jobs:
|
||||
- name: Publish
|
||||
run: yarn publish-from-github
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}
|
||||
GA_TRACKING_ID: ${{ secrets.GA_TRACKING_ID }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
|
||||
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()`
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
|
||||
## Usage
|
||||
|
||||
Vercel is the optimal workflow for frontend teams. All-in-one: Static and Jamstack deployment, Serverless Functions, and Global CDN.
|
||||
Vercel is a platform for **static sites and frontend frameworks**, built to integrate with your headless content, commerce, or database.
|
||||
|
||||
We provide a **frictionless developer experience** to take care of the hard things: deploy instantly, scale automatically, and serve personalized content around the globe.
|
||||
|
||||
We make it easy for frontend teams to **develop, preview, and ship** delightful user experiences, where performance is the default.
|
||||
|
||||
Get started by [Importing a Git Project](https://vercel.com/new) and use `git push` to deploy. Alternatively, you can [install Vercel CLI](https://vercel.com/cli).
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
* Get example list from extracted folder
|
||||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
import { lstatSync, existsSync, readdirSync } from 'fs';
|
||||
|
||||
const exists = (path: string) => existsSync(path);
|
||||
const isDotFile = (name: string) => name.startsWith('.');
|
||||
const isDirectory = (path: string) => lstatSync(path).isDirectory();
|
||||
|
||||
export function summary(source: string) {
|
||||
export function summary(source: string): string[] {
|
||||
if (!exists(source) || !isDirectory(source)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return readdirSync(source)
|
||||
.filter(name => !isDotFile(name))
|
||||
.filter(name => isDirectory(join(source, name)));
|
||||
return readdirSync(source, { withFileTypes: true })
|
||||
.filter(d => !isDotFile(d.name))
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ const frameworks = (_frameworks as Framework[])
|
||||
sort: undefined,
|
||||
dependency: undefined,
|
||||
defaultRoutes: undefined,
|
||||
devCommand: undefined,
|
||||
buildCommand: undefined,
|
||||
};
|
||||
|
||||
if (framework.logo) {
|
||||
|
||||
@@ -6,3 +6,5 @@ coverage:
|
||||
project: off
|
||||
patch: off
|
||||
|
||||
fixes:
|
||||
- "::packages/cli/" # move root e.g., "path/" => "after/path/"
|
||||
|
||||
13218
examples/angular/package-lock.json
generated
Normal file
13218
examples/angular/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,37 +12,38 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~8.1.0",
|
||||
"@angular/common": "~8.1.0",
|
||||
"@angular/compiler": "~8.1.0",
|
||||
"@angular/core": "~8.1.0",
|
||||
"@angular/forms": "~8.1.0",
|
||||
"@angular/platform-browser": "~8.1.0",
|
||||
"@angular/platform-browser-dynamic": "~8.1.0",
|
||||
"@angular/router": "~8.1.0",
|
||||
"@angular/animations": "^8.1.0",
|
||||
"@angular/common": "^8.1.0",
|
||||
"@angular/core": "^8.1.0",
|
||||
"@angular/forms": "^8.1.0",
|
||||
"@angular/platform-browser": "^8.1.0",
|
||||
"@angular/platform-browser-dynamic": "^8.1.0",
|
||||
"@angular/router": "^8.1.0",
|
||||
"rxjs": "~6.4.0",
|
||||
"tslib": "^1.9.0",
|
||||
"zone.js": "~0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.801.0",
|
||||
"@angular/cli": "~8.1.0",
|
||||
"@angular/compiler-cli": "~8.1.0",
|
||||
"@angular-devkit/build-angular": "^12.2.2",
|
||||
"@angular/cli": "^12.2.2",
|
||||
"@angular/compiler": "^12.2.2",
|
||||
"@angular/compiler-cli": "^12.2.2",
|
||||
"@angular/language-service": "~8.1.0",
|
||||
"@types/node": "~8.9.4",
|
||||
"@types/jasmine": "~3.3.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "^5.0.0",
|
||||
"jasmine-core": "~3.4.0",
|
||||
"glob-parent": "^5.1.2",
|
||||
"jasmine-core": "^3.4.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~4.1.0",
|
||||
"karma": "^6.3.4",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||
"karma-jasmine": "~2.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.4.0",
|
||||
"protractor": "~5.4.0",
|
||||
"protractor": "^7.0.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.15.0",
|
||||
"typescript": "~3.4.3"
|
||||
"typescript": "^4.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"dependencies": {
|
||||
"react": "^16.6.1",
|
||||
"react-dom": "^16.6.1",
|
||||
"react-scripts": "2.1.1"
|
||||
"react-scripts": "^4.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "BROWSER=none react-scripts start",
|
||||
|
||||
3
examples/nextjs/.eslintrc.json
Normal file
3
examples/nextjs/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -29,6 +29,6 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
|
||||
3
examples/nextjs/next.config.js
Normal file
3
examples/nextjs/next.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
{
|
||||
"name": "nextjs",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "10.x",
|
||||
"react": "17.x",
|
||||
"react-dom": "17.x"
|
||||
"next": "12.0.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-next": "11.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
|
||||
export default (req, res) => {
|
||||
res.statusCode = 200
|
||||
res.json({ name: 'John Doe' })
|
||||
export default function handler(req, res) {
|
||||
res.status(200).json({ name: 'John Doe' })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import styles from '../styles/Home.module.css'
|
||||
|
||||
export default function Home() {
|
||||
@@ -6,6 +7,7 @@ export default function Home() {
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
@@ -21,12 +23,12 @@ export default function Home() {
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a href="https://nextjs.org/docs" className={styles.card}>
|
||||
<h3>Documentation →</h3>
|
||||
<h2>Documentation →</h2>
|
||||
<p>Find in-depth information about Next.js features and API.</p>
|
||||
</a>
|
||||
|
||||
<a href="https://nextjs.org/learn" className={styles.card}>
|
||||
<h3>Learn →</h3>
|
||||
<h2>Learn →</h2>
|
||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
||||
</a>
|
||||
|
||||
@@ -34,15 +36,15 @@ export default function Home() {
|
||||
href="https://github.com/vercel/next.js/tree/master/examples"
|
||||
className={styles.card}
|
||||
>
|
||||
<h3>Examples →</h3>
|
||||
<h2>Examples →</h2>
|
||||
<p>Discover and deploy boilerplate example Next.js projects.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/import?filter=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
>
|
||||
<h3>Deploy →</h3>
|
||||
<h2>Deploy →</h2>
|
||||
<p>
|
||||
Instantly deploy your Next.js site to a public URL with Vercel.
|
||||
</p>
|
||||
@@ -57,7 +59,9 @@ export default function Home() {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by{' '}
|
||||
<img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
|
||||
<span className={styles.logo}>
|
||||
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
|
||||
</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -25,14 +26,11 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.title a {
|
||||
@@ -82,7 +80,6 @@
|
||||
|
||||
.card {
|
||||
margin: 1rem;
|
||||
flex-basis: 45%;
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
@@ -90,6 +87,7 @@
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
@@ -99,7 +97,7 @@
|
||||
border-color: #0070f3;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
.card h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
@@ -112,6 +110,7 @@
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
7
examples/parcel/.gitignore
vendored
Normal file
7
examples/parcel/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.parcel-cache
|
||||
dist
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.DS_Store
|
||||
.vercel
|
||||
34
examples/parcel/README.md
Normal file
34
examples/parcel/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
This is a vanilla web app built with [Parcel](https://parceljs.org).
|
||||
|
||||
## Getting Started
|
||||
|
||||
Run the dev server:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
# or
|
||||
npm start
|
||||
```
|
||||
|
||||
And open [localhost:1234](http://localhost:1234) in your browser!
|
||||
|
||||
As you make changes, you should see your app automatically update in the browser without even refreshing the page!
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Getting started guide](https://parceljs.org/getting-started/webapp/)
|
||||
- [Documentation](https://parceljs.org/docs/)
|
||||
|
||||
## Deploy Your Own
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/vercel/vercel/tree/main/examples/parcel&template=parcel)
|
||||
|
||||
_Live Example: https://parcel.examples.vercel.com_
|
||||
|
||||
### Deploying From Your Terminal
|
||||
|
||||
You can deploy your new Parcel project with a single command from your terminal using [Vercel CLI](https://vercel.com/download):
|
||||
|
||||
```shell
|
||||
$ vercel
|
||||
```
|
||||
12
examples/parcel/package.json
Normal file
12
examples/parcel/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "parcel-app",
|
||||
"private": true,
|
||||
"source": "src/index.html",
|
||||
"scripts": {
|
||||
"start": "parcel",
|
||||
"build": "parcel build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"parcel": "^2.0.0"
|
||||
}
|
||||
}
|
||||
1
examples/parcel/src/app.js
Normal file
1
examples/parcel/src/app.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log('Hello world!');
|
||||
13
examples/parcel/src/index.html
Normal file
13
examples/parcel/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>My First Parcel App</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<script type="module" src="app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello, Parcel!</h1>
|
||||
<p>This is a vanilla web app built with Parcel 2!<br>Check out the <a href="https://parceljs.org/getting-started/webapp/" target="_blank">getting started guide</a> and the <a href="https://parceljs.org/docs/" target="_blank">documentation</a> to learn more!</p>
|
||||
</body>
|
||||
</html>
|
||||
18
examples/parcel/src/styles.css
Normal file
18
examples/parcel/src/styles.css
Normal file
@@ -0,0 +1,18 @@
|
||||
body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: hotpink;
|
||||
font-family: cursive;
|
||||
font-size: 55px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: dodgerblue;
|
||||
}
|
||||
4541
examples/parcel/yarn.lock
Normal file
4541
examples/parcel/yarn.lock
Normal file
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
|
||||
57
examples/remix/README.md
Normal file
57
examples/remix/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Welcome to Remix!
|
||||
|
||||
- [Remix Docs](https://remix.run/docs)
|
||||
|
||||
## Vercel Setup
|
||||
|
||||
First you'll need the [Vercel CLI](https://vercel.com/docs/cli):
|
||||
|
||||
```sh
|
||||
npm i -g vercel
|
||||
```
|
||||
|
||||
Before you can run the app in development, you need to link this project to a new Vercel project on your account.
|
||||
|
||||
**It is important that you use a new project. If you try to link this project to an existing project (like a Next.js site) you will have problems.**
|
||||
|
||||
```sh
|
||||
$ vercel link
|
||||
```
|
||||
|
||||
Follow the prompts, and when it's done you should be able to get started.
|
||||
|
||||
## Development
|
||||
|
||||
You will be running two processes during development when using Vercel as your server.
|
||||
|
||||
- Your Vercel server in one
|
||||
- The Remix development server in another
|
||||
|
||||
```sh
|
||||
# in one tab
|
||||
$ vercel dev
|
||||
|
||||
# in another
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go!
|
||||
|
||||
If you'd rather run everything in a single tab, you can look at [concurrently](https://npm.im/concurrently) or similar tools to run both processes in one tab.
|
||||
|
||||
## Deploying
|
||||
|
||||
```sh
|
||||
$ npm run build
|
||||
# preview deployment
|
||||
$ vercel
|
||||
|
||||
# production deployment
|
||||
$ vercel --prod
|
||||
```
|
||||
|
||||
### GitHub Automatic Deployments
|
||||
|
||||
For some reason the GitHub integration doesn't deploy the public folder. We're working with Vercel to figure this out.
|
||||
|
||||
For now, [you can set up a GitHub action with this config](https://gist.github.com/mcansh/91f8effda798b41bb373351fad217070) from our friend [@mcansh](https://github.com/mcansh).
|
||||
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
|
||||
});
|
||||
}
|
||||
247
examples/remix/app/root.tsx
Normal file
247
examples/remix/app/root.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Link,
|
||||
Links,
|
||||
LiveReload,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useCatch,
|
||||
useLocation
|
||||
} from "remix";
|
||||
import type { LinksFunction } from "remix";
|
||||
|
||||
import deleteMeRemixStyles from "~/styles/demos/remix.css";
|
||||
import globalStylesUrl from "~/styles/global.css";
|
||||
import darkStylesUrl from "~/styles/dark.css";
|
||||
|
||||
/**
|
||||
* The `links` export is a function that returns an array of objects that map to
|
||||
* the attributes for an HTML `<link>` element. These will load `<link>` tags on
|
||||
* every route in the app, but individual routes can include their own links
|
||||
* that are automatically unloaded when a user navigates away from the route.
|
||||
*
|
||||
* https://remix.run/api/app#links
|
||||
*/
|
||||
export let links: LinksFunction = () => {
|
||||
return [
|
||||
{ rel: "stylesheet", href: globalStylesUrl },
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: darkStylesUrl,
|
||||
media: "(prefers-color-scheme: dark)"
|
||||
},
|
||||
{ rel: "stylesheet", href: deleteMeRemixStyles }
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* The root module's default export is a component that renders the current
|
||||
* route via the `<Outlet />` component. Think of this as the global layout
|
||||
* component for your app.
|
||||
*/
|
||||
export default function App() {
|
||||
return (
|
||||
<Document>
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
function Document({
|
||||
children,
|
||||
title
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
{title ? <title>{title}</title> : null}
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<RouteChangeAnnouncement />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
{process.env.NODE_ENV === "development" && <LiveReload />}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout({ children }: React.PropsWithChildren<{}>) {
|
||||
return (
|
||||
<div className="remix-app">
|
||||
<header className="remix-app__header">
|
||||
<div className="container remix-app__header-content">
|
||||
<Link to="/" title="Remix" className="remix-app__header-home-link">
|
||||
<RemixLogo />
|
||||
</Link>
|
||||
<nav aria-label="Main navigation" className="remix-app__header-nav">
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/">Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://remix.run/docs">Remix Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/remix-run/remix">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div className="remix-app__main">
|
||||
<div className="container remix-app__main-content">{children}</div>
|
||||
</div>
|
||||
<footer className="remix-app__footer">
|
||||
<div className="container remix-app__footer-content">
|
||||
<p>© You!</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CatchBoundary() {
|
||||
let caught = useCatch();
|
||||
|
||||
let message;
|
||||
switch (caught.status) {
|
||||
case 401:
|
||||
message = (
|
||||
<p>
|
||||
Oops! Looks like you tried to visit a page that you do not have access
|
||||
to.
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case 404:
|
||||
message = (
|
||||
<p>Oops! Looks like you tried to visit a page that does not exist.</p>
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(caught.data || caught.statusText);
|
||||
}
|
||||
|
||||
return (
|
||||
<Document title={`${caught.status} ${caught.statusText}`}>
|
||||
<Layout>
|
||||
<h1>
|
||||
{caught.status}: {caught.statusText}
|
||||
</h1>
|
||||
{message}
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: { error: Error }) {
|
||||
console.error(error);
|
||||
return (
|
||||
<Document title="Error!">
|
||||
<Layout>
|
||||
<div>
|
||||
<h1>There was an error</h1>
|
||||
<p>{error.message}</p>
|
||||
<hr />
|
||||
<p>
|
||||
Hey, developer, you should replace this with what you want your
|
||||
users to see.
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
function RemixLogo(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 659 165"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
aria-labelledby="remix-run-logo-title"
|
||||
role="img"
|
||||
width="106"
|
||||
height="30"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<title id="remix-run-logo-title">Remix Logo</title>
|
||||
<path d="M0 161V136H45.5416C53.1486 136 54.8003 141.638 54.8003 145V161H0Z M133.85 124.16C135.3 142.762 135.3 151.482 135.3 161H92.2283C92.2283 158.927 92.2653 157.03 92.3028 155.107C92.4195 149.128 92.5411 142.894 91.5717 130.304C90.2905 111.872 82.3473 107.776 67.7419 107.776H54.8021H0V74.24H69.7918C88.2407 74.24 97.4651 68.632 97.4651 53.784C97.4651 40.728 88.2407 32.816 69.7918 32.816H0V0H77.4788C119.245 0 140 19.712 140 51.2C140 74.752 125.395 90.112 105.665 92.672C122.32 96 132.057 105.472 133.85 124.16Z" />
|
||||
<path d="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" />
|
||||
<path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" />
|
||||
<path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" />
|
||||
<path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an alert for screen reader users when the route changes.
|
||||
*/
|
||||
const RouteChangeAnnouncement = React.memo(() => {
|
||||
let [hydrated, setHydrated] = React.useState(false);
|
||||
let [innerHtml, setInnerHtml] = React.useState("");
|
||||
let location = useLocation();
|
||||
|
||||
React.useEffect(() => {
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
let firstRenderRef = React.useRef(true);
|
||||
React.useEffect(() => {
|
||||
// Skip the first render because we don't want an announcement on the
|
||||
// initial page load.
|
||||
if (firstRenderRef.current) {
|
||||
firstRenderRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let pageTitle = location.pathname === "/" ? "Home page" : document.title;
|
||||
setInnerHtml(`Navigated to ${pageTitle}`);
|
||||
}, [location.pathname]);
|
||||
|
||||
// Render nothing on the server. The live region provides no value unless
|
||||
// scripts are loaded and the browser takes over normal routing.
|
||||
if (!hydrated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-live="assertive"
|
||||
aria-atomic
|
||||
id="route-change-region"
|
||||
style={{
|
||||
border: "0",
|
||||
clipPath: "inset(100%)",
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: "1px",
|
||||
margin: "-1px",
|
||||
overflow: "hidden",
|
||||
padding: "0",
|
||||
position: "absolute",
|
||||
width: "1px",
|
||||
whiteSpace: "nowrap",
|
||||
wordWrap: "normal"
|
||||
}}
|
||||
>
|
||||
{innerHtml}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
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;
|
||||
}
|
||||
120
examples/remix/app/styles/demos/remix.css
Normal file
120
examples/remix/app/styles/demos/remix.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* You probably want to just delete this file; it's just for the demo pages.
|
||||
*/
|
||||
.remix-app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: calc(100vh - env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.remix-app > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remix-app__header {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.remix-app__header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remix-app__header-home-link {
|
||||
width: 106px;
|
||||
height: 30px;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.remix-app__header-nav ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
.remix-app__header-nav li {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remix-app__main {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.remix-app__footer {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.remix-app__footer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remix__page {
|
||||
--gap: 1rem;
|
||||
--space: 2rem;
|
||||
display: grid;
|
||||
grid-auto-rows: min-content;
|
||||
gap: var(--gap);
|
||||
padding-top: var(--space);
|
||||
padding-bottom: var(--space);
|
||||
}
|
||||
|
||||
@media print, screen and (min-width: 640px) {
|
||||
.remix__page {
|
||||
--gap: 2rem;
|
||||
grid-auto-rows: unset;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.remix__page {
|
||||
--gap: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.remix__page > main > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.remix__page > main > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.remix__page > aside {
|
||||
margin: 0;
|
||||
padding: 1.5ch 2ch;
|
||||
border: solid 1px var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.remix__page > aside > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.remix__page > aside > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.remix__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.remix__form > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
98
examples/remix/app/styles/global.css
Normal file
98
examples/remix/app/styles/global.css
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* You can just delete everything here or keep whatever you like, it's just a
|
||||
* quick baseline!
|
||||
*/
|
||||
:root {
|
||||
--color-foreground: hsl(0, 0%, 7%);
|
||||
--color-background: hsl(0, 0%, 100%);
|
||||
--color-links: hsl(213, 100%, 52%);
|
||||
--color-links-hover: hsl(213, 100%, 43%);
|
||||
--color-border: hsl(0, 0%, 82%);
|
||||
--font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica, Roboto,
|
||||
Arial, sans-serif, system-ui, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: var(--color-links) solid 2px;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-links);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-links-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background-color: var(--color-border);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
input:where([type="text"]),
|
||||
input:where([type="search"]) {
|
||||
display: block;
|
||||
border: 1px solid var(--color-border);
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
height: calc(1ch + 1.5em);
|
||||
padding-right: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
background-color: hsl(0 0% 100% / 20%);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
--gutter: 16px;
|
||||
width: 1024px;
|
||||
max-width: calc(100% - var(--gutter) * 2);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
28
examples/remix/package.json
Normal file
28
examples/remix/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "remix-app-template",
|
||||
"description": "",
|
||||
"license": "",
|
||||
"scripts": {
|
||||
"build": "remix build",
|
||||
"dev": "remix dev",
|
||||
"postinstall": "remix setup node"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/react": "^1.0.4",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"remix": "^1.0.4",
|
||||
"@remix-run/vercel": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.0.4",
|
||||
"@types/react": "^17.0.24",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "4.28.0",
|
||||
"@typescript-eslint/parser": "4.28.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"async-retry": "1.2.3",
|
||||
"buffer-replace": "1.0.0",
|
||||
"cheerio": "1.0.0-rc.3",
|
||||
@@ -25,17 +24,19 @@
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-plugin-jest": "24.3.6",
|
||||
"husky": "6.0.0",
|
||||
"jest": "27.3.1",
|
||||
"json5": "2.1.1",
|
||||
"lint-staged": "9.2.5",
|
||||
"node-fetch": "2.6.1",
|
||||
"npm-package-arg": "6.1.0",
|
||||
"prettier": "2.3.1"
|
||||
"prettier": "2.3.1",
|
||||
"ts-jest": "27.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"lerna": "lerna",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"publish-stable": "echo 'Run `yarn changelog` for instructions'",
|
||||
"publish-canary": "git checkout main && git pull && lerna version prerelease --preid canary --message 'Publish Canary' --exact",
|
||||
"publish-canary": "git checkout main && git pull && lerna version prerelease --preid canary --message \"Publish Canary\" --exact",
|
||||
"publish-from-github": "./utils/publish.sh",
|
||||
"changelog": "node utils/changelog.js",
|
||||
"build": "node utils/run.js build all",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.12.0",
|
||||
"version": "2.12.3-canary.22",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
@@ -21,16 +21,16 @@
|
||||
"@types/async-retry": "^1.2.1",
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/end-of-stream": "^1.4.0",
|
||||
"@types/fs-extra": "^5.0.5",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/jest": "26.0.24",
|
||||
"@types/jest": "27.0.1",
|
||||
"@types/js-yaml": "3.12.1",
|
||||
"@types/ms": "0.7.31",
|
||||
"@types/multistream": "2.1.1",
|
||||
"@types/node-fetch": "^2.1.6",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "^2.4.1",
|
||||
"@vercel/frameworks": "0.5.0",
|
||||
"@vercel/frameworks": "0.5.1-canary.13",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"aggregate-error": "3.0.1",
|
||||
"async-retry": "1.2.3",
|
||||
@@ -38,16 +38,14 @@
|
||||
"boxen": "4.2.0",
|
||||
"cross-spawn": "6.0.5",
|
||||
"end-of-stream": "1.4.1",
|
||||
"fs-extra": "7.0.0",
|
||||
"fs-extra": "10.0.0",
|
||||
"glob": "7.1.3",
|
||||
"into-stream": "5.0.0",
|
||||
"jest": "27.0.6",
|
||||
"js-yaml": "3.13.1",
|
||||
"minimatch": "3.0.4",
|
||||
"multistream": "2.1.1",
|
||||
"node-fetch": "2.6.1",
|
||||
"semver": "6.1.1",
|
||||
"ts-jest": "27.0.4",
|
||||
"typescript": "4.3.4",
|
||||
"yazl": "2.4.3"
|
||||
}
|
||||
|
||||
313
packages/build-utils/src/convert-runtime-to-plugin.ts
Normal file
313
packages/build-utils/src/convert-runtime-to-plugin.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import fs from 'fs-extra';
|
||||
import { join, dirname, relative } from 'path';
|
||||
import glob from './fs/glob';
|
||||
import { normalizePath } from './fs/normalize-path';
|
||||
import { detectBuilders } from './detect-builders';
|
||||
import { FILES_SYMBOL, getLambdaOptionsFromFunction, Lambda } from './lambda';
|
||||
import type FileBlob from './file-blob';
|
||||
import type { BuilderFunctions, BuildOptions, Files } from './types';
|
||||
import minimatch from 'minimatch';
|
||||
|
||||
/**
|
||||
* Convert legacy Runtime to a Plugin.
|
||||
* @param buildRuntime - a legacy build() function from a Runtime
|
||||
* @param ext - the file extension, for example `.py`
|
||||
*/
|
||||
export function convertRuntimeToPlugin(
|
||||
buildRuntime: (options: BuildOptions) => Promise<{ output: Lambda }>,
|
||||
ext: string
|
||||
) {
|
||||
// This `build()` signature should match `plugin.build()` signature in `vercel build`.
|
||||
return async function build({
|
||||
vercelConfig,
|
||||
workPath,
|
||||
}: {
|
||||
vercelConfig: {
|
||||
functions?: BuilderFunctions;
|
||||
regions?: string[];
|
||||
trailingSlash?: boolean;
|
||||
cleanUrls?: boolean;
|
||||
};
|
||||
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 pages: { [key: string]: any } = {};
|
||||
const { functions = {}, cleanUrls, trailingSlash } = vercelConfig;
|
||||
const traceDir = join(workPath, '.output', 'runtime-traced-files');
|
||||
await fs.ensureDir(traceDir);
|
||||
|
||||
for (const entrypoint of Object.keys(entrypoints)) {
|
||||
const key =
|
||||
Object.keys(functions).find(
|
||||
src => src === entrypoint || minimatch(entrypoint, src)
|
||||
) || '';
|
||||
const config = functions[key] || {};
|
||||
|
||||
const { output } = await buildRuntime({
|
||||
files,
|
||||
entrypoint,
|
||||
workPath,
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
includeFiles: config.includeFiles,
|
||||
excludeFiles: config.excludeFiles,
|
||||
},
|
||||
});
|
||||
|
||||
pages[entrypoint] = {
|
||||
handler: output.handler,
|
||||
runtime: output.runtime,
|
||||
memory: output.memory,
|
||||
maxDuration: output.maxDuration,
|
||||
environment: output.environment,
|
||||
allowQuery: output.allowQuery,
|
||||
regions: output.regions,
|
||||
};
|
||||
|
||||
// @ts-ignore This symbol is a private API
|
||||
const lambdaFiles: Files = output[FILES_SYMBOL];
|
||||
|
||||
const entry = join(workPath, '.output', 'server', 'pages', entrypoint);
|
||||
await fs.ensureDir(dirname(entry));
|
||||
await linkOrCopy(files[entrypoint].fsPath, entry);
|
||||
|
||||
const tracedFiles: {
|
||||
absolutePath: string;
|
||||
relativePath: string;
|
||||
}[] = [];
|
||||
|
||||
Object.entries(lambdaFiles).forEach(async ([relPath, file]) => {
|
||||
const newPath = join(traceDir, relPath);
|
||||
tracedFiles.push({ absolutePath: newPath, relativePath: relPath });
|
||||
if (file.fsPath) {
|
||||
await linkOrCopy(file.fsPath, newPath);
|
||||
} else if (file.type === 'FileBlob') {
|
||||
const { data, mode } = file as FileBlob;
|
||||
await fs.writeFile(newPath, data, { mode });
|
||||
} else {
|
||||
throw new Error(`Unknown file type: ${file.type}`);
|
||||
}
|
||||
});
|
||||
|
||||
const nft = join(
|
||||
workPath,
|
||||
'.output',
|
||||
'server',
|
||||
'pages',
|
||||
`${entrypoint}.nft.json`
|
||||
);
|
||||
const json = JSON.stringify({
|
||||
version: 1,
|
||||
files: tracedFiles.map(f => ({
|
||||
input: normalizePath(relative(nft, f.absolutePath)),
|
||||
output: normalizePath(f.relativePath),
|
||||
})),
|
||||
});
|
||||
|
||||
await fs.ensureDir(dirname(nft));
|
||||
await fs.writeFile(nft, json);
|
||||
}
|
||||
|
||||
await updateFunctionsManifest({ vercelConfig, workPath, pages });
|
||||
|
||||
const {
|
||||
warnings,
|
||||
errors,
|
||||
//defaultRoutes,
|
||||
redirectRoutes,
|
||||
//rewriteRoutes,
|
||||
dynamicRoutesWithKeys,
|
||||
// errorRoutes, already handled by pages404
|
||||
} = await detectBuilders(Object.keys(files), null, {
|
||||
tag: 'latest',
|
||||
functions: functions,
|
||||
projectSettings: undefined,
|
||||
featHandleMiss: true,
|
||||
cleanUrls,
|
||||
trailingSlash,
|
||||
});
|
||||
|
||||
if (errors) {
|
||||
throw new Error(errors[0].message);
|
||||
}
|
||||
|
||||
if (warnings) {
|
||||
warnings.forEach(warning => console.warn(warning.message, warning.link));
|
||||
}
|
||||
|
||||
const redirects = redirectRoutes
|
||||
?.filter(r => r.src && 'headers' in r)
|
||||
?.map(r => ({
|
||||
source: r.src || '',
|
||||
destination:
|
||||
'headers' in r && r.headers?.Location ? r.headers.Location : '',
|
||||
statusCode: 'status' in r && r.status ? r.status : 307,
|
||||
regex: r.src || '',
|
||||
}));
|
||||
|
||||
const dynamicRoutes = dynamicRoutesWithKeys?.map(r => {
|
||||
const keys = Object.keys(r.routeKeys);
|
||||
return {
|
||||
page: '/' + r.fileName.slice(0, -ext.length),
|
||||
regex: r.regex,
|
||||
routeKeys: r.routeKeys,
|
||||
namedRegex: r.regex
|
||||
.split('([^/]+)')
|
||||
.map((str, i) => str + (keys[i] ? `(?<${keys[i]}>[^/]+)` : ''))
|
||||
.join(''),
|
||||
};
|
||||
});
|
||||
|
||||
await updateRoutesManifest({
|
||||
workPath,
|
||||
redirects,
|
||||
rewrites: [],
|
||||
dynamicRoutes,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function linkOrCopy(existingPath: string, newPath: string) {
|
||||
try {
|
||||
await fs.createLink(existingPath, newPath);
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
await fs.copyFile(existingPath, newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readJson(filePath: string): Promise<{ [key: string]: any }> {
|
||||
try {
|
||||
const str = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(str);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return {};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `.output/functions-manifest.json` exists, append to the pages
|
||||
* property. Otherwise write a new file. This will also read `vercel.json`
|
||||
* and apply relevant `functions` property config.
|
||||
*/
|
||||
export async function updateFunctionsManifest({
|
||||
vercelConfig,
|
||||
workPath,
|
||||
pages,
|
||||
}: {
|
||||
vercelConfig: { functions?: BuilderFunctions; regions?: string[] };
|
||||
workPath: string;
|
||||
pages: { [key: string]: any };
|
||||
}) {
|
||||
const functionsManifestPath = join(
|
||||
workPath,
|
||||
'.output',
|
||||
'functions-manifest.json'
|
||||
);
|
||||
const functionsManifest = await readJson(functionsManifestPath);
|
||||
|
||||
if (!functionsManifest.version) functionsManifest.version = 1;
|
||||
if (!functionsManifest.pages) functionsManifest.pages = {};
|
||||
|
||||
for (const [pageKey, pageConfig] of Object.entries(pages)) {
|
||||
const fnConfig = await getLambdaOptionsFromFunction({
|
||||
sourceFile: pageKey,
|
||||
config: vercelConfig,
|
||||
});
|
||||
functionsManifest.pages[pageKey] = {
|
||||
...pageConfig,
|
||||
memory: fnConfig.memory || pageConfig.memory,
|
||||
maxDuration: fnConfig.maxDuration || pageConfig.maxDuration,
|
||||
regions: vercelConfig.regions || pageConfig.regions,
|
||||
};
|
||||
}
|
||||
|
||||
await fs.writeFile(functionsManifestPath, JSON.stringify(functionsManifest));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
@@ -16,6 +16,12 @@ interface ErrorResponse {
|
||||
link?: string;
|
||||
}
|
||||
|
||||
interface DynamicRoutesWithKeys {
|
||||
fileName: string;
|
||||
regex: string;
|
||||
routeKeys: { [key: string]: string };
|
||||
}
|
||||
|
||||
interface Options {
|
||||
tag?: 'canary' | 'latest' | string;
|
||||
functions?: BuilderFunctions;
|
||||
@@ -96,6 +102,7 @@ export async function detectBuilders(
|
||||
redirectRoutes: Route[] | null;
|
||||
rewriteRoutes: Route[] | null;
|
||||
errorRoutes: Route[] | null;
|
||||
dynamicRoutesWithKeys: DynamicRoutesWithKeys[] | null;
|
||||
}> {
|
||||
const errors: ErrorResponse[] = [];
|
||||
const warnings: ErrorResponse[] = [];
|
||||
@@ -114,6 +121,7 @@ export async function detectBuilders(
|
||||
redirectRoutes: null,
|
||||
rewriteRoutes: null,
|
||||
errorRoutes: null,
|
||||
dynamicRoutesWithKeys: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,13 +165,14 @@ export async function detectBuilders(
|
||||
|
||||
const apiRoutes: Source[] = [];
|
||||
const dynamicRoutes: Source[] = [];
|
||||
const dynamicRoutesWithKeys: DynamicRoutesWithKeys[] = [];
|
||||
|
||||
// API
|
||||
for (const fileName of sortedFiles) {
|
||||
const apiBuilder = maybeGetApiBuilder(fileName, apiMatches, options);
|
||||
|
||||
if (apiBuilder) {
|
||||
const { routeError, apiRoute, isDynamic } = getApiRoute(
|
||||
const { routeError, apiRoute, isDynamic, routeKeys } = getApiRoute(
|
||||
fileName,
|
||||
apiSortedFiles,
|
||||
options,
|
||||
@@ -179,6 +188,7 @@ export async function detectBuilders(
|
||||
redirectRoutes: null,
|
||||
rewriteRoutes: null,
|
||||
errorRoutes: null,
|
||||
dynamicRoutesWithKeys: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -186,6 +196,11 @@ export async function detectBuilders(
|
||||
apiRoutes.push(apiRoute);
|
||||
if (isDynamic) {
|
||||
dynamicRoutes.push(apiRoute);
|
||||
dynamicRoutesWithKeys.push({
|
||||
fileName,
|
||||
regex: apiRoute.src,
|
||||
routeKeys,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +272,7 @@ export async function detectBuilders(
|
||||
defaultRoutes: null,
|
||||
rewriteRoutes: null,
|
||||
errorRoutes: null,
|
||||
dynamicRoutesWithKeys: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -299,6 +315,7 @@ export async function detectBuilders(
|
||||
defaultRoutes: null,
|
||||
rewriteRoutes: null,
|
||||
errorRoutes: null,
|
||||
dynamicRoutesWithKeys: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -342,6 +359,7 @@ export async function detectBuilders(
|
||||
defaultRoutes: routesResult.defaultRoutes,
|
||||
rewriteRoutes: routesResult.rewriteRoutes,
|
||||
errorRoutes: routesResult.errorRoutes,
|
||||
dynamicRoutesWithKeys,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -675,6 +693,7 @@ function getApiRoute(
|
||||
): {
|
||||
apiRoute: Source | null;
|
||||
isDynamic: boolean;
|
||||
routeKeys: { [key: string]: string };
|
||||
routeError: ErrorResponse | null;
|
||||
} {
|
||||
const conflictingSegment = getConflictingSegment(fileName);
|
||||
@@ -683,6 +702,7 @@ function getApiRoute(
|
||||
return {
|
||||
apiRoute: null,
|
||||
isDynamic: false,
|
||||
routeKeys: {},
|
||||
routeError: {
|
||||
code: 'conflicting_path_segment',
|
||||
message:
|
||||
@@ -703,6 +723,7 @@ function getApiRoute(
|
||||
return {
|
||||
apiRoute: null,
|
||||
isDynamic: false,
|
||||
routeKeys: {},
|
||||
routeError: {
|
||||
code: 'conflicting_file_path',
|
||||
message:
|
||||
@@ -722,6 +743,7 @@ function getApiRoute(
|
||||
return {
|
||||
apiRoute: out.route,
|
||||
isDynamic: out.isDynamic,
|
||||
routeKeys: out.routeKeys,
|
||||
routeError: null,
|
||||
};
|
||||
}
|
||||
@@ -867,11 +889,12 @@ function createRouteFromPath(
|
||||
filePath: string,
|
||||
featHandleMiss: boolean,
|
||||
cleanUrls: boolean
|
||||
): { route: Source; isDynamic: boolean } {
|
||||
): { route: Source; isDynamic: boolean; routeKeys: { [key: string]: string } } {
|
||||
const parts = filePath.split('/');
|
||||
|
||||
let counter = 1;
|
||||
const query: string[] = [];
|
||||
const routeKeys: { [key: string]: string } = {};
|
||||
let isDynamic = false;
|
||||
|
||||
const srcParts = parts.map((segment, i): string => {
|
||||
@@ -881,6 +904,7 @@ function createRouteFromPath(
|
||||
if (name !== null) {
|
||||
// We can't use `URLSearchParams` because `$` would get escaped
|
||||
query.push(`${name}=$${counter++}`);
|
||||
routeKeys[name] = name;
|
||||
isDynamic = true;
|
||||
return `([^/]+)`;
|
||||
} else if (isLast) {
|
||||
@@ -929,7 +953,7 @@ function createRouteFromPath(
|
||||
};
|
||||
}
|
||||
|
||||
return { route, isDynamic };
|
||||
return { route, isDynamic, routeKeys };
|
||||
}
|
||||
|
||||
function getRouteResult(
|
||||
@@ -1030,7 +1054,7 @@ function getRouteResult(
|
||||
// https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
errorRoutes.push({
|
||||
status: 404,
|
||||
src: '^/(?!.*api).*$',
|
||||
src: '^(?!/api).*$',
|
||||
dest: options.cleanUrls ? '/404' : '/404.html',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,4 +100,4 @@ class FileFsRef implements File {
|
||||
}
|
||||
}
|
||||
|
||||
export = FileFsRef;
|
||||
export default FileFsRef;
|
||||
|
||||
@@ -3,6 +3,7 @@ import assert from 'assert';
|
||||
import vanillaGlob_ from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import { lstat, Stats } from 'fs-extra';
|
||||
import { normalizePath } from './normalize-path';
|
||||
import FileFsRef from '../file-fs-ref';
|
||||
|
||||
export type GlobOptions = vanillaGlob_.IOptions;
|
||||
@@ -45,7 +46,7 @@ export default async function glob(
|
||||
const files = await vanillaGlob(pattern, options);
|
||||
|
||||
for (const relativePath of files) {
|
||||
const fsPath = path.join(options.cwd!, relativePath).replace(/\\/g, '/');
|
||||
const fsPath = normalizePath(path.join(options.cwd!, relativePath));
|
||||
let stat: Stats = options.statCache![fsPath] as Stats;
|
||||
assert(
|
||||
stat,
|
||||
|
||||
8
packages/build-utils/src/fs/normalize-path.ts
Normal file
8
packages/build-utils/src/fs/normalize-path.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
const isWin = process.platform === 'win32';
|
||||
|
||||
/**
|
||||
* Convert Windows separators to Unix separators.
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
return isWin ? p.replace(/\\/g, '/') : p;
|
||||
}
|
||||
@@ -244,10 +244,13 @@ export async function scanParentDirs(
|
||||
const packageJsonPath = path.join(currentDestPath, 'package.json');
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (readPackageJson) {
|
||||
// Only read the contents of the *first* `package.json` file found,
|
||||
// since that's the one related to this installation.
|
||||
if (readPackageJson && !packageJson) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [packageLockJson, hasYarnLock] = await Promise.all([
|
||||
fs
|
||||
@@ -256,9 +259,8 @@ export async function scanParentDirs(
|
||||
// If the file doesn't exist, fail gracefully otherwise error
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}),
|
||||
fs.pathExists(path.join(currentDestPath, 'yarn.lock')),
|
||||
]);
|
||||
@@ -267,7 +269,13 @@ export async function scanParentDirs(
|
||||
cliType = 'npm';
|
||||
lockfileVersion = packageLockJson.lockfileVersion;
|
||||
}
|
||||
break;
|
||||
|
||||
// Only stop iterating if a lockfile was found, because it's possible
|
||||
// that the lockfile is in a higher path than where the `package.json`
|
||||
// file was found.
|
||||
if (packageLockJson || hasYarnLock) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const newDestPath = path.dirname(currentDestPath);
|
||||
@@ -305,7 +313,8 @@ export async function runNpmInstall(
|
||||
destPath: string,
|
||||
args: string[] = [],
|
||||
spawnOpts?: SpawnOptions,
|
||||
meta?: Meta
|
||||
meta?: Meta,
|
||||
nodeVersion?: NodeVersion
|
||||
) {
|
||||
if (meta?.isDev) {
|
||||
debug('Skipping dependency installation because dev mode is enabled');
|
||||
@@ -329,7 +338,12 @@ export async function runNpmInstall(
|
||||
.filter(a => a !== '--prefer-offline')
|
||||
.concat(['install', '--no-audit', '--unsafe-perm']);
|
||||
|
||||
if (typeof lockfileVersion === 'number' && lockfileVersion >= 2) {
|
||||
// If the lockfile version is 2 or greater and the node version is less than 16 than we will force npm7 to be used
|
||||
if (
|
||||
typeof lockfileVersion === 'number' &&
|
||||
lockfileVersion >= 2 &&
|
||||
(nodeVersion?.major || 0) < 16
|
||||
) {
|
||||
// Ensure that npm 7 is at the beginning of the `$PATH`
|
||||
env.PATH = `/node16/bin-npm7:${env.PATH}`;
|
||||
console.log('Detected `package-lock.json` generated by npm 7...');
|
||||
|
||||
@@ -81,6 +81,12 @@ export {
|
||||
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,
|
||||
updateFunctionsManifest,
|
||||
updateRoutesManifest,
|
||||
} from './convert-runtime-to-plugin';
|
||||
|
||||
export * from './schemas';
|
||||
export * from './types';
|
||||
|
||||
@@ -19,6 +19,8 @@ interface LambdaOptions {
|
||||
memory?: number;
|
||||
maxDuration?: number;
|
||||
environment: Environment;
|
||||
allowQuery?: string[];
|
||||
regions?: string[];
|
||||
}
|
||||
|
||||
interface CreateLambdaOptions {
|
||||
@@ -28,13 +30,17 @@ interface CreateLambdaOptions {
|
||||
memory?: number;
|
||||
maxDuration?: number;
|
||||
environment?: Environment;
|
||||
allowQuery?: string[];
|
||||
regions?: string[];
|
||||
}
|
||||
|
||||
interface GetLambdaOptionsFromFunctionOptions {
|
||||
sourceFile: string;
|
||||
config?: Config;
|
||||
config?: Pick<Config, 'functions'>;
|
||||
}
|
||||
|
||||
export const FILES_SYMBOL = Symbol('files');
|
||||
|
||||
export class Lambda {
|
||||
public type: 'Lambda';
|
||||
public zipBuffer: Buffer;
|
||||
@@ -43,6 +49,8 @@ export class Lambda {
|
||||
public memory?: number;
|
||||
public maxDuration?: number;
|
||||
public environment: Environment;
|
||||
public allowQuery?: string[];
|
||||
public regions?: string[];
|
||||
|
||||
constructor({
|
||||
zipBuffer,
|
||||
@@ -51,6 +59,8 @@ export class Lambda {
|
||||
maxDuration,
|
||||
memory,
|
||||
environment,
|
||||
allowQuery,
|
||||
regions,
|
||||
}: LambdaOptions) {
|
||||
this.type = 'Lambda';
|
||||
this.zipBuffer = zipBuffer;
|
||||
@@ -59,6 +69,8 @@ export class Lambda {
|
||||
this.memory = memory;
|
||||
this.maxDuration = maxDuration;
|
||||
this.environment = environment;
|
||||
this.allowQuery = allowQuery;
|
||||
this.regions = regions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +84,8 @@ export async function createLambda({
|
||||
memory,
|
||||
maxDuration,
|
||||
environment = {},
|
||||
allowQuery,
|
||||
regions,
|
||||
}: CreateLambdaOptions): Promise<Lambda> {
|
||||
assert(typeof files === 'object', '"files" must be an object');
|
||||
assert(typeof handler === 'string', '"handler" is not a string');
|
||||
@@ -86,18 +100,38 @@ export async function createLambda({
|
||||
assert(typeof maxDuration === 'number', '"maxDuration" is not a number');
|
||||
}
|
||||
|
||||
if (allowQuery !== undefined) {
|
||||
assert(Array.isArray(allowQuery), '"allowQuery" is not an Array');
|
||||
assert(
|
||||
allowQuery.every(q => typeof q === 'string'),
|
||||
'"allowQuery" is not a string Array'
|
||||
);
|
||||
}
|
||||
|
||||
if (regions !== undefined) {
|
||||
assert(Array.isArray(regions), '"regions" is not an Array');
|
||||
assert(
|
||||
regions.every(r => typeof r === 'string'),
|
||||
'"regions" is not a string Array'
|
||||
);
|
||||
}
|
||||
|
||||
await sema.acquire();
|
||||
|
||||
try {
|
||||
const zipBuffer = await createZip(files);
|
||||
return new Lambda({
|
||||
const lambda = new Lambda({
|
||||
zipBuffer,
|
||||
handler,
|
||||
runtime,
|
||||
memory,
|
||||
maxDuration,
|
||||
environment,
|
||||
regions,
|
||||
});
|
||||
// @ts-ignore This symbol is a private API
|
||||
lambda[FILES_SYMBOL] = files;
|
||||
return lambda;
|
||||
} finally {
|
||||
sema.release();
|
||||
}
|
||||
@@ -131,9 +165,7 @@ export async function createZip(files: Files): Promise<Buffer> {
|
||||
}
|
||||
|
||||
zipFile.end();
|
||||
streamToBuffer(zipFile.outputStream)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
streamToBuffer(zipFile.outputStream).then(resolve).catch(reject);
|
||||
});
|
||||
|
||||
return zipBuffer;
|
||||
|
||||
@@ -9,6 +9,7 @@ interface PrerenderOptions {
|
||||
fallback: FileBlob | FileFsRef | FileRef | null;
|
||||
group?: number;
|
||||
bypassToken?: string | null /* optional to be non-breaking change */;
|
||||
allowQuery?: string[];
|
||||
}
|
||||
|
||||
export class Prerender {
|
||||
@@ -18,6 +19,7 @@ export class Prerender {
|
||||
public fallback: FileBlob | FileFsRef | FileRef | null;
|
||||
public group?: number;
|
||||
public bypassToken: string | null;
|
||||
public allowQuery?: string[];
|
||||
|
||||
constructor({
|
||||
expiration,
|
||||
@@ -25,6 +27,7 @@ export class Prerender {
|
||||
fallback,
|
||||
group,
|
||||
bypassToken,
|
||||
allowQuery,
|
||||
}: PrerenderOptions) {
|
||||
this.type = 'Prerender';
|
||||
this.expiration = expiration;
|
||||
@@ -62,5 +65,19 @@ export class Prerender {
|
||||
);
|
||||
}
|
||||
this.fallback = fallback;
|
||||
|
||||
if (allowQuery !== undefined) {
|
||||
if (!Array.isArray(allowQuery)) {
|
||||
throw new Error(
|
||||
'The `allowQuery` argument for `Prerender` must be Array.'
|
||||
);
|
||||
}
|
||||
if (!allowQuery.every(q => typeof q === 'string')) {
|
||||
throw new Error(
|
||||
'The `allowQuery` argument for `Prerender` must be Array of strings.'
|
||||
);
|
||||
}
|
||||
this.allowQuery = allowQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface File {
|
||||
mode: number;
|
||||
contentType?: string;
|
||||
toStream: () => NodeJS.ReadableStream;
|
||||
toStreamAsync?: () => Promise<NodeJS.ReadableStream>;
|
||||
/**
|
||||
* The absolute path to the file in the filesystem
|
||||
*/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# users.rb
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/users.rb": {
|
||||
"memory": 3008
|
||||
},
|
||||
"api/doesnt-exist.rb": {
|
||||
"memory": 768
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# [id].py
|
||||
@@ -0,0 +1 @@
|
||||
# index
|
||||
@@ -0,0 +1 @@
|
||||
# project/[aid]/[bid]/index.py
|
||||
@@ -0,0 +1 @@
|
||||
# get
|
||||
@@ -0,0 +1 @@
|
||||
# post
|
||||
@@ -0,0 +1 @@
|
||||
This file should also be included
|
||||
@@ -0,0 +1 @@
|
||||
# date
|
||||
@@ -0,0 +1 @@
|
||||
# math
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/users/post.py": {
|
||||
"memory": 3008
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "package.json", "use": "@vercel/static-build" }],
|
||||
"probes": [{ "path": "/", "mustContain": "npm version: 7" }]
|
||||
"probes": [{ "path": "/", "mustContain": "npm version: 8" }]
|
||||
}
|
||||
|
||||
15
packages/build-utils/test/fixtures/21-npm-workspaces/a/package.json
vendored
Normal file
15
packages/build-utils/test/fixtures/21-npm-workspaces/a/package.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "a",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.2"
|
||||
}
|
||||
}
|
||||
15
packages/build-utils/test/fixtures/21-npm-workspaces/b/package.json
vendored
Normal file
15
packages/build-utils/test/fixtures/21-npm-workspaces/b/package.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "b",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cowsay": "^1.5.0"
|
||||
}
|
||||
}
|
||||
782
packages/build-utils/test/fixtures/21-npm-workspaces/package-lock.json
generated
vendored
Normal file
782
packages/build-utils/test/fixtures/21-npm-workspaces/package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,782 @@
|
||||
{
|
||||
"name": "21-npm-workspaces",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "1.0.0",
|
||||
"workspaces": [
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
},
|
||||
"a": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"b": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cowsay": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/a": {
|
||||
"resolved": "a",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/b": {
|
||||
"resolved": "b",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/cowsay": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.5.0.tgz",
|
||||
"integrity": "sha512-8Ipzr54Z8zROr/62C8f0PdhQcDusS05gKTS87xxdji8VbWefWly0k8BwGK7+VqamOrkv3eGsCkPtvlHzrhWsCA==",
|
||||
"dependencies": {
|
||||
"get-stdin": "8.0.0",
|
||||
"string-width": "~2.1.1",
|
||||
"strip-final-newline": "2.0.0",
|
||||
"yargs": "15.4.1"
|
||||
},
|
||||
"bin": {
|
||||
"cowsay": "cli.js",
|
||||
"cowthink": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stdin": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
|
||||
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"dependencies": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/string-width": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi/node_modules/strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"a": {
|
||||
"version": "file:a",
|
||||
"requires": {
|
||||
"debug": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"b": {
|
||||
"version": "file:b",
|
||||
"requires": {
|
||||
"cowsay": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
||||
},
|
||||
"cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"cowsay": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.5.0.tgz",
|
||||
"integrity": "sha512-8Ipzr54Z8zROr/62C8f0PdhQcDusS05gKTS87xxdji8VbWefWly0k8BwGK7+VqamOrkv3eGsCkPtvlHzrhWsCA==",
|
||||
"requires": {
|
||||
"get-stdin": "8.0.0",
|
||||
"string-width": "~2.1.1",
|
||||
"strip-final-newline": "2.0.0",
|
||||
"yargs": "15.4.1"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"requires": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"get-stdin": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
|
||||
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"requires": {
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"requires": {
|
||||
"p-limit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
|
||||
},
|
||||
"require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"requires": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"strip-final-newline": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
|
||||
},
|
||||
"which-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"requires": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/build-utils/test/fixtures/21-npm-workspaces/package.json
vendored
Normal file
8
packages/build-utils/test/fixtures/21-npm-workspaces/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "21-npm-workspaces",
|
||||
"version": "1.0.0",
|
||||
"workspaces": [
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
}
|
||||
@@ -30,6 +30,7 @@ const skipFixtures: string[] = [
|
||||
'06-zero-config-hugo',
|
||||
'07-zero-config-jekyll',
|
||||
'08-zero-config-middleman',
|
||||
'21-npm-workspaces',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
@@ -2013,15 +2013,11 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
|
||||
framework: 'redwoodjs',
|
||||
};
|
||||
|
||||
const {
|
||||
builders,
|
||||
defaultRoutes,
|
||||
rewriteRoutes,
|
||||
errorRoutes,
|
||||
} = await detectBuilders(files, null, {
|
||||
projectSettings,
|
||||
featHandleMiss,
|
||||
});
|
||||
const { builders, defaultRoutes, rewriteRoutes, errorRoutes } =
|
||||
await detectBuilders(files, null, {
|
||||
projectSettings,
|
||||
featHandleMiss,
|
||||
});
|
||||
|
||||
expect(builders).toStrictEqual([
|
||||
{
|
||||
@@ -2038,7 +2034,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
|
||||
expect(errorRoutes).toStrictEqual([
|
||||
{
|
||||
status: 404,
|
||||
src: '^/(?!.*api).*$',
|
||||
src: '^(?!/api).*$',
|
||||
dest: '/404.html',
|
||||
},
|
||||
]);
|
||||
@@ -2050,15 +2046,11 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
|
||||
framework: 'redwoodjs',
|
||||
};
|
||||
|
||||
const {
|
||||
builders,
|
||||
defaultRoutes,
|
||||
rewriteRoutes,
|
||||
errorRoutes,
|
||||
} = await detectBuilders(files, null, {
|
||||
projectSettings,
|
||||
featHandleMiss,
|
||||
});
|
||||
const { builders, defaultRoutes, rewriteRoutes, errorRoutes } =
|
||||
await detectBuilders(files, null, {
|
||||
projectSettings,
|
||||
featHandleMiss,
|
||||
});
|
||||
|
||||
expect(builders).toStrictEqual([
|
||||
{
|
||||
@@ -2096,7 +2088,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
|
||||
expect(errorRoutes).toStrictEqual([
|
||||
{
|
||||
status: 404,
|
||||
src: '^/(?!.*api).*$',
|
||||
src: '^(?!/api).*$',
|
||||
dest: '/404.html',
|
||||
},
|
||||
]);
|
||||
@@ -2417,7 +2409,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
expect(errorRoutes).toStrictEqual([
|
||||
{
|
||||
status: 404,
|
||||
src: '^/(?!.*api).*$',
|
||||
src: '^(?!/api).*$',
|
||||
dest: '/404.html',
|
||||
},
|
||||
]);
|
||||
@@ -2435,6 +2427,11 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
'/another/sub/index.html',
|
||||
'/another/sub/page.html',
|
||||
'/another/sub/page',
|
||||
'/another/api',
|
||||
'/another/api/page.html',
|
||||
'/rapid',
|
||||
'/rapid/page.html',
|
||||
'/health-api.html',
|
||||
].forEach(file => {
|
||||
expect(file).toMatch(pattern);
|
||||
});
|
||||
@@ -2443,12 +2440,12 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
|
||||
'/api',
|
||||
'/api/',
|
||||
'/api/index.html',
|
||||
'/api/page.html',
|
||||
'/api/page',
|
||||
'/api/users.js',
|
||||
'/api/users',
|
||||
'/api/sub',
|
||||
'/api/sub/index.html',
|
||||
'/api/sub/page.html',
|
||||
'/api/sub/page',
|
||||
'/api/sub/users.js',
|
||||
'/api/sub/users',
|
||||
].forEach(file => {
|
||||
expect(file).not.toMatch(pattern);
|
||||
});
|
||||
@@ -2819,12 +2816,8 @@ 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 } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -2836,7 +2829,7 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
expect(errorRoutes).toStrictEqual([
|
||||
{
|
||||
status: 404,
|
||||
src: '^/(?!.*api).*$',
|
||||
src: '^(?!/api).*$',
|
||||
dest: '/404',
|
||||
},
|
||||
]);
|
||||
@@ -2904,11 +2897,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -2936,11 +2926,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
'api/[endpoint]/[id].js',
|
||||
];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -2974,11 +2961,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
|
||||
const files = ['public/index.html', 'api/[endpoint].js'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, pkg, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, pkg, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3004,11 +2988,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
const files = ['api/date/index.js', 'api/date.js'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3022,11 +3003,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
{
|
||||
const files = ['api/date.js', 'api/[date]/index.js'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3051,11 +3029,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
'api/food.ts',
|
||||
'api/ts/gold.ts',
|
||||
];
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3071,11 +3046,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/user.php'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, { functions, ...options });
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, { functions, ...options });
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3105,11 +3077,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3152,11 +3121,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3184,11 +3150,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
'api/[endpoint]/[id].js',
|
||||
];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3222,11 +3185,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
|
||||
const files = ['public/index.html', 'api/[endpoint].js'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, pkg, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, pkg, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3245,11 +3205,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
const files = ['api/date/index.js', 'api/date.js'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3263,11 +3220,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
{
|
||||
const files = ['api/date.js', 'api/[date]/index.js'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3292,11 +3246,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
'api/food.ts',
|
||||
'api/ts/gold.ts',
|
||||
];
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, options);
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, options);
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
@@ -3312,11 +3263,8 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
|
||||
const functions = { 'api/user.php': { runtime: 'vercel-php@0.1.0' } };
|
||||
const files = ['api/user.php'];
|
||||
|
||||
const {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
rewriteRoutes,
|
||||
} = await detectBuilders(files, null, { functions, ...options });
|
||||
const { defaultRoutes, redirectRoutes, rewriteRoutes } =
|
||||
await detectBuilders(files, null, { functions, ...options });
|
||||
testHeaders(redirectRoutes);
|
||||
expect(defaultRoutes).toStrictEqual([]);
|
||||
expect(rewriteRoutes).toStrictEqual([
|
||||
|
||||
280
packages/build-utils/test/unit.convert-runtime-to-plugin.test.ts
vendored
Normal file
280
packages/build-utils/test/unit.convert-runtime-to-plugin.test.ts
vendored
Normal file
@@ -0,0 +1,280 @@
|
||||
import { join } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { BuildOptions, createLambda } from '../src';
|
||||
import { convertRuntimeToPlugin } from '../src/convert-runtime-to-plugin';
|
||||
|
||||
async function fsToJson(dir: string, output: Record<string, any> = {}) {
|
||||
const files = await fs.readdir(dir);
|
||||
for (const file of files) {
|
||||
const fsPath = join(dir, file);
|
||||
const stat = await fs.stat(fsPath);
|
||||
if (stat.isDirectory()) {
|
||||
output[file] = {};
|
||||
await fsToJson(fsPath, output[file]);
|
||||
} else {
|
||||
output[file] = await fs.readFile(fsPath, 'utf8');
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
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(invalidFuncWorkpath, '.output'));
|
||||
await fs.remove(join(pythonApiWorkpath, '.output'));
|
||||
});
|
||||
|
||||
it('should error with invalid functions prop', async () => {
|
||||
const workPath = invalidFuncWorkpath;
|
||||
const lambdaOptions = {
|
||||
handler: 'index.handler',
|
||||
runtime: 'nodejs14.x',
|
||||
memory: 512,
|
||||
maxDuration: 5,
|
||||
environment: {},
|
||||
regions: ['sfo1'],
|
||||
};
|
||||
|
||||
const buildRuntime = async (opts: BuildOptions) => {
|
||||
const lambda = await createLambda({
|
||||
files: opts.files,
|
||||
...lambdaOptions,
|
||||
});
|
||||
return { output: lambda };
|
||||
};
|
||||
|
||||
const lambdaFiles = await fsToJson(workPath);
|
||||
const vercelConfig = JSON.parse(lambdaFiles['vercel.json']);
|
||||
delete lambdaFiles['vercel.json'];
|
||||
const build = await convertRuntimeToPlugin(buildRuntime, '.js');
|
||||
|
||||
expect(build({ vercelConfig, workPath })).rejects.toThrow(
|
||||
new Error(
|
||||
'The pattern "api/doesnt-exist.rb" defined in `functions` doesn\'t match any Serverless Functions inside the `api` directory.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should create correct fileystem for python', async () => {
|
||||
const workPath = pythonApiWorkpath;
|
||||
const lambdaOptions = {
|
||||
handler: 'index.handler',
|
||||
runtime: 'python3.9',
|
||||
memory: 512,
|
||||
maxDuration: 5,
|
||||
environment: {},
|
||||
regions: ['sfo1'],
|
||||
};
|
||||
|
||||
const buildRuntime = async (opts: BuildOptions) => {
|
||||
const lambda = await createLambda({
|
||||
files: opts.files,
|
||||
...lambdaOptions,
|
||||
});
|
||||
return { output: lambda };
|
||||
};
|
||||
|
||||
const lambdaFiles = await fsToJson(workPath);
|
||||
const vercelConfig = JSON.parse(lambdaFiles['vercel.json']);
|
||||
delete lambdaFiles['vercel.json'];
|
||||
const build = await convertRuntimeToPlugin(buildRuntime, '.py');
|
||||
|
||||
await build({ vercelConfig, workPath });
|
||||
|
||||
const output = await fsToJson(join(workPath, '.output'));
|
||||
expect(output).toMatchObject({
|
||||
'functions-manifest.json': expect.stringContaining('{'),
|
||||
'routes-manifest.json': expect.stringContaining('{'),
|
||||
'runtime-traced-files': lambdaFiles,
|
||||
server: {
|
||||
pages: {
|
||||
api: {
|
||||
'index.py': expect.stringContaining('index'),
|
||||
'index.py.nft.json': expect.stringContaining('{'),
|
||||
users: {
|
||||
'get.py': expect.stringContaining('get'),
|
||||
'get.py.nft.json': expect.stringContaining('{'),
|
||||
'post.py': expect.stringContaining('post'),
|
||||
'post.py.nft.json': expect.stringContaining('{'),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const funcManifest = JSON.parse(output['functions-manifest.json']);
|
||||
expect(funcManifest).toMatchObject({
|
||||
version: 1,
|
||||
pages: {
|
||||
'api/index.py': lambdaOptions,
|
||||
'api/users/get.py': lambdaOptions,
|
||||
'api/users/post.py': { ...lambdaOptions, memory: 3008 },
|
||||
},
|
||||
});
|
||||
|
||||
const routesManifest = JSON.parse(output['routes-manifest.json']);
|
||||
expect(routesManifest).toMatchObject({
|
||||
version: 3,
|
||||
pages404: true,
|
||||
redirects: [],
|
||||
rewrites: [
|
||||
/* TODO: `handle: miss`
|
||||
{
|
||||
source: "^/api/(.+)(?:\\.(?:py))$",
|
||||
destination: "/api/db/[id]?id=$1",
|
||||
regex: "^/api/(.+)(?:\\.(?:py))$"
|
||||
}
|
||||
*/
|
||||
],
|
||||
dynamicRoutes: [
|
||||
{
|
||||
page: '/api/project/[aid]/[bid]/index',
|
||||
regex: '^/api/project/([^/]+)/([^/]+)(/|/index|/index\\.py)?$',
|
||||
routeKeys: { aid: 'aid', bid: 'bid' },
|
||||
namedRegex:
|
||||
'^/api/project/(?<aid>[^/]+)/(?<bid>[^/]+)(/|/index|/index\\.py)?$',
|
||||
},
|
||||
{
|
||||
page: '/api/db/[id]',
|
||||
regex: '^/api/db/([^/]+)$',
|
||||
routeKeys: { id: 'id' },
|
||||
namedRegex: '^/api/db/(?<id>[^/]+)$',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const indexJson = JSON.parse(output.server.pages.api['index.py.nft.json']);
|
||||
expect(indexJson).toMatchObject({
|
||||
version: 1,
|
||||
files: [
|
||||
{
|
||||
input: '../../../../runtime-traced-files/api/db/[id].py',
|
||||
output: 'api/db/[id].py',
|
||||
},
|
||||
{
|
||||
input: '../../../../runtime-traced-files/api/index.py',
|
||||
output: 'api/index.py',
|
||||
},
|
||||
{
|
||||
input:
|
||||
'../../../../runtime-traced-files/api/project/[aid]/[bid]/index.py',
|
||||
output: 'api/project/[aid]/[bid]/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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const getJson = JSON.parse(
|
||||
output.server.pages.api.users['get.py.nft.json']
|
||||
);
|
||||
expect(getJson).toMatchObject({
|
||||
version: 1,
|
||||
files: [
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/db/[id].py',
|
||||
output: 'api/db/[id].py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/index.py',
|
||||
output: 'api/index.py',
|
||||
},
|
||||
{
|
||||
input:
|
||||
'../../../../../runtime-traced-files/api/project/[aid]/[bid]/index.py',
|
||||
output: 'api/project/[aid]/[bid]/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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const postJson = JSON.parse(
|
||||
output.server.pages.api.users['post.py.nft.json']
|
||||
);
|
||||
expect(postJson).toMatchObject({
|
||||
version: 1,
|
||||
files: [
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/db/[id].py',
|
||||
output: 'api/db/[id].py',
|
||||
},
|
||||
{
|
||||
input: '../../../../../runtime-traced-files/api/index.py',
|
||||
output: 'api/index.py',
|
||||
},
|
||||
{
|
||||
input:
|
||||
'../../../../../runtime-traced-files/api/project/[aid]/[bid]/index.py',
|
||||
output: 'api/project/[aid]/[bid]/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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(output.server.pages['file.txt']).toBeUndefined();
|
||||
expect(output.server.pages.api['file.txt']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
26
packages/build-utils/test/unit.test.ts
vendored
26
packages/build-utils/test/unit.test.ts
vendored
@@ -298,23 +298,29 @@ it(
|
||||
);
|
||||
|
||||
it('should return lockfileVersion 2 with npm7', async () => {
|
||||
const packageLockJsonPath = path.join(__dirname, 'fixtures', '20-npm-7');
|
||||
const result = await scanParentDirs(packageLockJsonPath);
|
||||
const fixture = path.join(__dirname, 'fixtures', '20-npm-7');
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(2);
|
||||
});
|
||||
|
||||
it('should not return lockfileVersion with yarn', async () => {
|
||||
const packageLockJsonPath = path.join(__dirname, 'fixtures', '19-yarn-v2');
|
||||
const result = await scanParentDirs(packageLockJsonPath);
|
||||
const fixture = path.join(__dirname, 'fixtures', '19-yarn-v2');
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('yarn');
|
||||
expect(result.lockfileVersion).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return lockfileVersion 1 with older versions of npm', async () => {
|
||||
const packageLockJsonPath = path.join(
|
||||
__dirname,
|
||||
'fixtures',
|
||||
'08-yarn-npm/with-npm'
|
||||
);
|
||||
const result = await scanParentDirs(packageLockJsonPath);
|
||||
const fixture = path.join(__dirname, 'fixtures', '08-yarn-npm/with-npm');
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(1);
|
||||
});
|
||||
|
||||
it('should detect npm Workspaces', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '21-npm-workspaces/a');
|
||||
const result = await scanParentDirs(fixture);
|
||||
expect(result.cliType).toEqual('npm');
|
||||
expect(result.lockfileVersion).toEqual(2);
|
||||
});
|
||||
|
||||
11
packages/cli/@types/intercept-stdout/index.d.ts
vendored
11
packages/cli/@types/intercept-stdout/index.d.ts
vendored
@@ -1,11 +0,0 @@
|
||||
declare module 'intercept-stdout' {
|
||||
export default function (fn?: InterceptFn): UnhookIntercept
|
||||
}
|
||||
|
||||
interface InterceptFn {
|
||||
(text: string): string | void
|
||||
}
|
||||
|
||||
interface UnhookIntercept {
|
||||
(): void
|
||||
}
|
||||
5
packages/cli/@types/promisepipe/index.d.ts
vendored
5
packages/cli/@types/promisepipe/index.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
declare module 'promisepipe' {
|
||||
export default function (
|
||||
...streams: Array<NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream>
|
||||
): Promise<void>
|
||||
}
|
||||
@@ -10,9 +10,13 @@
|
||||
|
||||
## Usage
|
||||
|
||||
Vercel is the optimal workflow for frontend teams. All-in-one: Static and Jamstack deployment, Serverless Functions, and Global CDN.
|
||||
Vercel is a platform for **static sites and frontend frameworks**, built to integrate with your headless content, commerce, or database.
|
||||
|
||||
To install the latest version of Vercel CLI, visit [vercel.com/download](https://vercel.com/download) or run this command:
|
||||
We provide a **frictionless developer experience** to take care of the hard things: deploy instantly, scale automatically, and serve personalized content around the globe.
|
||||
|
||||
We make it easy for frontend teams to **develop, preview, and ship** delightful user experiences, where performance is the default.
|
||||
|
||||
To install the latest version of Vercel CLI, run this command:
|
||||
|
||||
```bash
|
||||
npm i -g vercel
|
||||
@@ -26,6 +30,8 @@ cd <PROJECT> # Change directory to the new project
|
||||
vercel # Deploy to the cloud
|
||||
```
|
||||
|
||||
Finally, [connect your Git repository to Vercel](https://vercel.com/docs/git) and deploy with `git push`.
|
||||
|
||||
## Documentation
|
||||
|
||||
For details on how to use Vercel CLI, check out our [documentation](https://vercel.com/docs).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "23.1.0",
|
||||
"version": "23.1.3-canary.40",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -12,33 +12,15 @@
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node ./scripts/preinstall.js",
|
||||
"test-unit": "nyc ava test/unit.js test/dev-builder.unit.js test/dev-router.unit.js test/dev-server.unit.js test/dev-validate.unit.js --serial --fail-fast --verbose",
|
||||
"test": "jest",
|
||||
"test-unit": "jest --coverage --verbose",
|
||||
"test-integration-cli": "rimraf test/fixtures/integration && ava test/integration.js --serial --fail-fast --verbose",
|
||||
"test-integration-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
|
||||
"prepublishOnly": "yarn build",
|
||||
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
|
||||
"coverage": "codecov",
|
||||
"build": "node -r ts-eager/register ./scripts/build.ts",
|
||||
"build-dev": "node -r ts-eager/register ./scripts/build.ts --dev"
|
||||
},
|
||||
"nyc": {
|
||||
"include": [
|
||||
"src/**"
|
||||
],
|
||||
"extension": [
|
||||
".js",
|
||||
".ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
],
|
||||
"reporter": [
|
||||
"text",
|
||||
"html"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"instrument": true,
|
||||
"all": true
|
||||
},
|
||||
"bin": {
|
||||
"vc": "./dist/index.js",
|
||||
"vercel": "./dist/index.js"
|
||||
@@ -61,14 +43,17 @@
|
||||
"node": ">= 12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.12.0",
|
||||
"@vercel/go": "1.2.3",
|
||||
"@vercel/node": "1.12.0",
|
||||
"@vercel/python": "2.0.5",
|
||||
"@vercel/ruby": "1.2.7",
|
||||
"update-notifier": "4.1.0"
|
||||
"@vercel/build-utils": "2.12.3-canary.22",
|
||||
"@vercel/go": "1.2.4-canary.4",
|
||||
"@vercel/node": "1.12.2-canary.7",
|
||||
"@vercel/python": "2.1.2-canary.0",
|
||||
"@vercel/ruby": "1.2.8-canary.4",
|
||||
"update-notifier": "4.1.0",
|
||||
"vercel-plugin-middleware": "0.0.0-canary.7",
|
||||
"vercel-plugin-node": "1.12.2-canary.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/env": "11.1.2",
|
||||
"@sentry/node": "5.5.0",
|
||||
"@sindresorhus/slugify": "0.11.0",
|
||||
"@tootallnate/once": "1.1.2",
|
||||
@@ -76,13 +61,16 @@
|
||||
"@types/ansi-regex": "4.0.0",
|
||||
"@types/async-retry": "1.2.1",
|
||||
"@types/bytes": "3.0.0",
|
||||
"@types/chance": "1.1.3",
|
||||
"@types/debug": "0.0.31",
|
||||
"@types/dotenv": "6.1.1",
|
||||
"@types/escape-html": "0.0.20",
|
||||
"@types/fs-extra": "5.0.5",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/glob": "7.1.1",
|
||||
"@types/http-proxy": "1.16.2",
|
||||
"@types/inquirer": "7.3.1",
|
||||
"@types/jest": "27.0.1",
|
||||
"@types/load-json-file": "2.0.7",
|
||||
"@types/mime-types": "2.1.0",
|
||||
"@types/minimatch": "3.0.3",
|
||||
@@ -97,11 +85,14 @@
|
||||
"@types/semver": "6.0.1",
|
||||
"@types/tar-fs": "1.16.1",
|
||||
"@types/text-table": "0.2.0",
|
||||
"@types/title": "3.4.1",
|
||||
"@types/universal-analytics": "0.4.2",
|
||||
"@types/update-notifier": "5.1.0",
|
||||
"@types/which": "1.3.2",
|
||||
"@types/write-json-file": "2.2.1",
|
||||
"@vercel/frameworks": "0.5.0",
|
||||
"@vercel/frameworks": "0.5.1-canary.13",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.17.0",
|
||||
"@zeit/fun": "0.11.2",
|
||||
"@zeit/source-map-support": "0.6.2",
|
||||
"ajv": "6.12.2",
|
||||
@@ -115,6 +106,7 @@
|
||||
"ava": "2.2.0",
|
||||
"bytes": "3.0.0",
|
||||
"chalk": "4.1.0",
|
||||
"chance": "1.1.7",
|
||||
"chokidar": "3.3.1",
|
||||
"clipboardy": "2.1.0",
|
||||
"codecov": "3.8.2",
|
||||
@@ -130,8 +122,9 @@
|
||||
"escape-html": "1.0.3",
|
||||
"esm": "3.1.4",
|
||||
"execa": "3.2.0",
|
||||
"express": "4.17.1",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "7.0.1",
|
||||
"fs-extra": "10.0.0",
|
||||
"get-port": "5.1.1",
|
||||
"glob": "7.1.2",
|
||||
"http-proxy": "1.18.1",
|
||||
@@ -148,7 +141,6 @@
|
||||
"ms": "2.1.2",
|
||||
"node-fetch": "2.6.1",
|
||||
"npm-package-arg": "6.1.0",
|
||||
"nyc": "13.2.0",
|
||||
"open": "8.2.0",
|
||||
"ora": "3.4.0",
|
||||
"pcre-to-regexp": "1.0.0",
|
||||
@@ -161,7 +153,6 @@
|
||||
"rimraf": "3.0.2",
|
||||
"semver": "5.5.0",
|
||||
"serve-handler": "6.1.1",
|
||||
"sinon": "4.4.2",
|
||||
"strip-ansi": "5.2.0",
|
||||
"stripe": "5.1.0",
|
||||
"tar-fs": "1.16.3",
|
||||
@@ -178,5 +169,19 @@
|
||||
"which": "2.0.2",
|
||||
"write-json-file": "2.2.0",
|
||||
"xdg-app-paths": "5.1.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"diagnostics": false,
|
||||
"isolatedModules": true
|
||||
}
|
||||
},
|
||||
"verbose": false,
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { join } from 'path';
|
||||
import { remove, writeFile } from 'fs-extra';
|
||||
|
||||
const dirRoot = join(__dirname, '..');
|
||||
const distRoot = join(dirRoot, 'dist');
|
||||
|
||||
async function createConstants() {
|
||||
console.log('Creating constants.ts');
|
||||
@@ -48,13 +49,12 @@ async function main() {
|
||||
|
||||
// Do the initial `ncc` build
|
||||
console.log();
|
||||
const src = join(dirRoot, 'src');
|
||||
const args = ['ncc', 'build', '--external', 'update-notifier'];
|
||||
if (isDev) {
|
||||
args.push('--source-map');
|
||||
}
|
||||
args.push(src);
|
||||
await execa('yarn', args, { stdio: 'inherit' });
|
||||
args.push('src/index.ts');
|
||||
await execa('yarn', args, { stdio: 'inherit', cwd: dirRoot });
|
||||
|
||||
// `ncc` has some issues with `@zeit/fun`'s runtime files:
|
||||
// - Executable bits on the `bootstrap` files appear to be lost:
|
||||
@@ -72,19 +72,13 @@ async function main() {
|
||||
dirRoot,
|
||||
'../../node_modules/@zeit/fun/dist/src/runtimes'
|
||||
);
|
||||
const dest = join(dirRoot, 'dist/runtimes');
|
||||
await cpy('**/*', dest, { parents: true, cwd: runtimes });
|
||||
await cpy('**/*', join(distRoot, 'runtimes'), {
|
||||
parents: true,
|
||||
cwd: runtimes,
|
||||
});
|
||||
|
||||
// Band-aid to delete stuff that `ncc` bundles, but it shouldn't:
|
||||
|
||||
// TypeScript definition files from `@vercel/build-utils`
|
||||
await remove(join(dirRoot, 'dist', 'dist'));
|
||||
|
||||
// The Readme and `package.json` from "config-chain" module
|
||||
await remove(join(dirRoot, 'dist', 'config-chain'));
|
||||
|
||||
// A bunch of source `.ts` files from CLI's `util` directory
|
||||
await remove(join(dirRoot, 'dist', 'util'));
|
||||
// Band-aid to bundle stuff that `ncc` neglects to bundle
|
||||
await cpy(join(dirRoot, 'src/util/projects/VERCEL_DIR_README.txt'), distRoot);
|
||||
|
||||
console.log('Finished building Vercel CLI');
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ function handleCreateAliasError<T>(
|
||||
return error;
|
||||
}
|
||||
|
||||
function getTargetsForAlias(args: string[], { alias }: VercelConfig) {
|
||||
function getTargetsForAlias(args: string[], { alias }: VercelConfig = {}) {
|
||||
if (args.length) {
|
||||
return [args[args.length - 1]]
|
||||
.map(target => (target.indexOf('.') !== -1 ? toHost(target) : target))
|
||||
|
||||
@@ -6,12 +6,11 @@ import cardBrands from '../../util/billing/card-brands';
|
||||
import success from '../../util/output/success';
|
||||
import wait from '../../util/output/wait';
|
||||
import chars from '../../util/output/chars';
|
||||
import rightPad from '../../util/output/right-pad';
|
||||
import error from '../../util/output/error';
|
||||
|
||||
const expDateMiddleware = data => data;
|
||||
|
||||
export default async function({ creditCards, clear = false, contextName }) {
|
||||
export default async function ({ creditCards, clear = false, contextName }) {
|
||||
const state = {
|
||||
error: undefined,
|
||||
cardGroupLabel: `> ${chalk.bold(
|
||||
@@ -19,13 +18,13 @@ export default async function({ creditCards, clear = false, contextName }) {
|
||||
)}`,
|
||||
|
||||
name: {
|
||||
label: rightPad('Full Name', 12),
|
||||
label: 'Full Name'.padEnd(12),
|
||||
placeholder: 'John Appleseed',
|
||||
validateValue: data => data.trim().length > 0,
|
||||
},
|
||||
|
||||
cardNumber: {
|
||||
label: rightPad('Number', 12),
|
||||
label: 'Number'.padEnd(12),
|
||||
mask: 'cc',
|
||||
placeholder: '#### #### #### ####',
|
||||
validateKeypress: (data, value) => /\d/.test(data) && value.length < 19,
|
||||
@@ -40,7 +39,7 @@ export default async function({ creditCards, clear = false, contextName }) {
|
||||
},
|
||||
|
||||
ccv: {
|
||||
label: rightPad('CCV', 12),
|
||||
label: 'CCV'.padEnd(12),
|
||||
mask: 'ccv',
|
||||
placeholder: '###',
|
||||
validateValue: data => {
|
||||
@@ -50,7 +49,7 @@ export default async function({ creditCards, clear = false, contextName }) {
|
||||
},
|
||||
|
||||
expDate: {
|
||||
label: rightPad('Exp. Date', 12),
|
||||
label: 'Exp. Date'.padEnd(12),
|
||||
mask: 'expDate',
|
||||
placeholder: 'mm / yyyy',
|
||||
middleware: expDateMiddleware,
|
||||
@@ -147,9 +146,9 @@ export default async function({ creditCards, clear = false, contextName }) {
|
||||
|
||||
console.log(
|
||||
success(
|
||||
`${state.cardNumber.brand ||
|
||||
state.cardNumber.card.brand} ending in ${res.last4 ||
|
||||
res.card.last4} was added to ${chalk.bold(contextName)}`
|
||||
`${state.cardNumber.brand || state.cardNumber.card.brand} ending in ${
|
||||
res.last4 || res.card.last4
|
||||
} was added to ${chalk.bold(contextName)}`
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import ms from 'ms';
|
||||
import plural from 'pluralize';
|
||||
import { error } from '../../util/error';
|
||||
import NowCreditCards from '../../util/credit-cards';
|
||||
import indent from '../../util/indent';
|
||||
import indent from '../../util/output/indent';
|
||||
import listInput from '../../util/input/list';
|
||||
import success from '../../util/output/success';
|
||||
import promptBool from '../../util/input/prompt-bool';
|
||||
@@ -51,8 +51,6 @@ const help = () => {
|
||||
};
|
||||
|
||||
let argv;
|
||||
let debug;
|
||||
let apiUrl;
|
||||
let subcommand;
|
||||
|
||||
export default async client => {
|
||||
@@ -65,8 +63,6 @@ export default async client => {
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
|
||||
debug = argv['--debug'];
|
||||
apiUrl = client.apiUrl;
|
||||
subcommand = argv._[0];
|
||||
|
||||
if (argv['--help'] || !subcommand) {
|
||||
@@ -76,17 +72,13 @@ export default async client => {
|
||||
|
||||
const {
|
||||
output,
|
||||
authConfig: { token },
|
||||
config: { currentTeam },
|
||||
} = client;
|
||||
|
||||
const start = new Date();
|
||||
const creditCards = new NowCreditCards({
|
||||
apiUrl,
|
||||
token,
|
||||
debug,
|
||||
client,
|
||||
currentTeam,
|
||||
output,
|
||||
});
|
||||
|
||||
let contextName = null;
|
||||
|
||||
803
packages/cli/src/commands/build.ts
Normal file
803
packages/cli/src/commands/build.ts
Normal file
@@ -0,0 +1,803 @@
|
||||
import { loadEnvConfig, processEnv } from '@next/env';
|
||||
import {
|
||||
execCommand,
|
||||
getScriptName,
|
||||
GlobOptions,
|
||||
scanParentDirs,
|
||||
spawnAsync,
|
||||
} 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 pluralize from 'pluralize';
|
||||
import Client from '../util/client';
|
||||
import { VercelConfig } from '../util/dev/types';
|
||||
import { emoji, prependEmoji } from '../util/emoji';
|
||||
import getArgs from '../util/get-args';
|
||||
import handleError from '../util/handle-error';
|
||||
import confirm from '../util/input/confirm';
|
||||
import { isSettingValue } from '../util/is-setting-value';
|
||||
import cmd from '../util/output/cmd';
|
||||
import logo from '../util/output/logo';
|
||||
import param from '../util/output/param';
|
||||
import stamp from '../util/output/stamp';
|
||||
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 { readProjectSettings } from '../util/projects/project-settings';
|
||||
import pull from './pull';
|
||||
|
||||
const sema = new Sema(16, {
|
||||
capacity: 100,
|
||||
});
|
||||
|
||||
const help = () => {
|
||||
return console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} build`)}
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
|
||||
'FILE'
|
||||
)} Path to the local ${'`vercel.json`'} file
|
||||
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
|
||||
'DIR'
|
||||
)} Path to the global ${'`.vercel`'} directory
|
||||
--cwd [path] The current working directory
|
||||
-d, --debug Debug mode [off]
|
||||
-y, --yes Skip the confirmation prompt
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Build the project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} build`)}
|
||||
${chalk.cyan(`$ ${getPkgName()} build --cwd ./path-to-project`)}
|
||||
`);
|
||||
};
|
||||
|
||||
const OUTPUT_DIR = '.output';
|
||||
|
||||
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 {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--debug': Boolean,
|
||||
'--cwd': String,
|
||||
});
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
|
||||
let cwd = argv['--cwd'] || process.cwd();
|
||||
|
||||
let project = await readProjectSettings(join(cwd, VERCEL_DIR));
|
||||
// If there are no project settings, only then do we pull them down
|
||||
while (!project?.settings) {
|
||||
const confirmed = await confirm(
|
||||
`No Project Settings found locally. Run ${getCommandName(
|
||||
'pull'
|
||||
)} for retrieving them?`,
|
||||
true
|
||||
);
|
||||
if (!confirmed) {
|
||||
client.output.print(`Aborted. No Project Settings retrieved.\n`);
|
||||
return 0;
|
||||
}
|
||||
const result = await pull(client);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
project = await readProjectSettings(join(cwd, VERCEL_DIR));
|
||||
}
|
||||
|
||||
// If `rootDirectory` exists, then `baseDir` will be the repo's root directory.
|
||||
const baseDir = cwd;
|
||||
|
||||
cwd = project.settings.rootDirectory
|
||||
? join(cwd, project.settings.rootDirectory)
|
||||
: cwd;
|
||||
|
||||
// Load the environment
|
||||
const { combinedEnv, loadedEnvFiles } = loadEnvConfig(cwd, false, {
|
||||
info: () => ({}), // we don't want to log this yet.
|
||||
error: (...args: any[]) => client.output.error(args.join(' ')),
|
||||
});
|
||||
|
||||
// Set process.env with loaded environment variables
|
||||
await processEnv(loadedEnvFiles);
|
||||
|
||||
const spawnOpts = {
|
||||
env: { ...combinedEnv, VERCEL: '1' },
|
||||
};
|
||||
|
||||
process.chdir(cwd);
|
||||
|
||||
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....
|
||||
if (framework === undefined) {
|
||||
client.output.error(
|
||||
`Framework detection failed or is malformed. Please run ${getCommandName(
|
||||
'pull'
|
||||
)} again.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const buildState = { ...project.settings };
|
||||
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
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
buildState.outputDirectory =
|
||||
project.settings.outputDirectory ||
|
||||
(isSettingValue(framework.settings.outputDirectory)
|
||||
? framework.settings.outputDirectory.value
|
||||
: null);
|
||||
buildState.rootDirectory = project.settings.rootDirectory;
|
||||
|
||||
if (loadedEnvFiles.length > 0) {
|
||||
console.log(
|
||||
`Loaded Environment Variables from ${loadedEnvFiles.length} ${pluralize(
|
||||
'file',
|
||||
loadedEnvFiles.length
|
||||
)}:`
|
||||
);
|
||||
for (let envFile of loadedEnvFiles) {
|
||||
console.log(chalk.dim(` - ${envFile.path}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Load plugins
|
||||
const debug = argv['--debug'];
|
||||
let plugins;
|
||||
try {
|
||||
plugins = await loadCliPlugins(cwd, client.output);
|
||||
} catch (error) {
|
||||
client.output.error('Failed to load CLI Plugins');
|
||||
handleError(error, { debug });
|
||||
return 1;
|
||||
}
|
||||
|
||||
const origLog = console.log;
|
||||
const origErr = console.error;
|
||||
const prefixedLog = (
|
||||
prefix: string,
|
||||
args: any[],
|
||||
logger: (...args: any[]) => void
|
||||
) => {
|
||||
if (typeof args[0] === 'string') {
|
||||
args[0] = `${prefix} ${args[0]}`;
|
||||
} else {
|
||||
args.unshift(prefix);
|
||||
}
|
||||
return logger(...args);
|
||||
};
|
||||
|
||||
if (plugins?.pluginCount && plugins?.pluginCount > 0) {
|
||||
console.log(
|
||||
`Loaded ${plugins.pluginCount} CLI ${pluralize(
|
||||
'Plugin',
|
||||
plugins.pluginCount
|
||||
)}`
|
||||
);
|
||||
// preBuild Plugins
|
||||
if (plugins.preBuildPlugins.length > 0) {
|
||||
console.log(
|
||||
`Running ${plugins.pluginCount} CLI ${pluralize(
|
||||
'Plugin',
|
||||
plugins.pluginCount
|
||||
)} before Build Command:`
|
||||
);
|
||||
for (let item of plugins.preBuildPlugins) {
|
||||
const { name, plugin, color } = item;
|
||||
if (typeof plugin.preBuild === 'function') {
|
||||
const pluginStamp = stamp();
|
||||
const fullName = name + '.preBuild';
|
||||
const prefix = chalk.gray(' > ') + color(fullName + ':');
|
||||
client.output.debug(`Running ${fullName}:`);
|
||||
try {
|
||||
console.log = (...args: any[]) =>
|
||||
prefixedLog(prefix, args, origLog);
|
||||
console.error = (...args: any[]) =>
|
||||
prefixedLog(prefix, args, origErr);
|
||||
await plugin.preBuild();
|
||||
client.output.debug(
|
||||
`Completed ${fullName} ${chalk.dim(`${pluginStamp()}`)}`
|
||||
);
|
||||
} catch (error) {
|
||||
client.output.error(`${prefix} failed`);
|
||||
handleError(error, { debug });
|
||||
return 1;
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
console.error = origErr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean the output directory
|
||||
fs.removeSync(join(cwd, OUTPUT_DIR));
|
||||
|
||||
// Yarn v2 PnP mode may be activated, so force
|
||||
// "node-modules" linker style
|
||||
const env = {
|
||||
YARN_NODE_LINKER: 'node-modules',
|
||||
...spawnOpts.env,
|
||||
};
|
||||
|
||||
if (typeof buildState.buildCommand === 'string') {
|
||||
console.log(`Running Build Command: ${cmd(buildState.buildCommand)}`);
|
||||
await execCommand(buildState.buildCommand, {
|
||||
...spawnOpts,
|
||||
env,
|
||||
cwd,
|
||||
});
|
||||
} else if (fs.existsSync(join(cwd, 'package.json'))) {
|
||||
await runPackageJsonScript(
|
||||
client,
|
||||
cwd,
|
||||
['vercel-build', 'now-build', 'build'],
|
||||
spawnOpts
|
||||
);
|
||||
} else if (typeof framework.settings.buildCommand.value === 'string') {
|
||||
console.log(
|
||||
`Running Build Command: ${cmd(framework.settings.buildCommand.value)}`
|
||||
);
|
||||
await execCommand(framework.settings.buildCommand.value, {
|
||||
...spawnOpts,
|
||||
env,
|
||||
cwd,
|
||||
});
|
||||
}
|
||||
// don't trust framework detection here because they might be switching to next on a branch
|
||||
const isNextJs = fs.existsSync(join(cwd, '.next'));
|
||||
|
||||
if (!fs.existsSync(join(cwd, OUTPUT_DIR))) {
|
||||
let outputDir = join(OUTPUT_DIR, 'static');
|
||||
let distDir = await framework.getFsOutputDir(cwd);
|
||||
if (isNextJs) {
|
||||
outputDir = OUTPUT_DIR;
|
||||
}
|
||||
const copyStamp = stamp();
|
||||
await fs.ensureDir(join(cwd, outputDir));
|
||||
const relativeDistDir = relative(cwd, distDir);
|
||||
client.output.spinner(
|
||||
`Copying files from ${param(distDir)} to ${param(outputDir)}`
|
||||
);
|
||||
const files = await glob(join(relativeDistDir, '**'), {
|
||||
ignore: [
|
||||
'node_modules/**',
|
||||
'.vercel/**',
|
||||
'.env',
|
||||
'.env.*',
|
||||
'.*ignore',
|
||||
'_middleware.ts',
|
||||
'_middleware.mts',
|
||||
'_middleware.cts',
|
||||
'_middleware.mjs',
|
||||
'_middleware.cjs',
|
||||
'_middleware.js',
|
||||
'api/**',
|
||||
'.git/**',
|
||||
'.next/cache/**',
|
||||
],
|
||||
nodir: true,
|
||||
dot: true,
|
||||
cwd,
|
||||
absolute: true,
|
||||
});
|
||||
await Promise.all(
|
||||
files.map(f =>
|
||||
smartCopy(
|
||||
client,
|
||||
f,
|
||||
distDir === '.'
|
||||
? join(cwd, outputDir, relative(cwd, f))
|
||||
: f.replace(distDir, outputDir)
|
||||
)
|
||||
)
|
||||
);
|
||||
client.output.stopSpinner();
|
||||
console.log(
|
||||
`Copied ${files.length.toLocaleString()} files from ${param(
|
||||
distDir
|
||||
)} to ${param(outputDir)} ${copyStamp()}`
|
||||
);
|
||||
|
||||
const buildManifestPath = join(cwd, OUTPUT_DIR, 'build-manifest.json');
|
||||
const routesManifestPath = join(cwd, OUTPUT_DIR, 'routes-manifest.json');
|
||||
|
||||
if (!fs.existsSync(buildManifestPath)) {
|
||||
client.output.debug(
|
||||
`Generating build manifest: ${param(buildManifestPath)}`
|
||||
);
|
||||
const buildManifest = {
|
||||
cache: framework.cachePattern ? [framework.cachePattern] : [],
|
||||
};
|
||||
await fs.writeJSON(buildManifestPath, buildManifest, { spaces: 2 });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(routesManifestPath)) {
|
||||
client.output.debug(
|
||||
`Generating routes manifest: ${param(routesManifestPath)}`
|
||||
);
|
||||
const routesManifest = {
|
||||
version: 3,
|
||||
pages404: true,
|
||||
basePath: '',
|
||||
redirects: framework.defaultRedirects ?? [],
|
||||
headers: framework.defaultHeaders ?? [],
|
||||
dynamicRoutes: [],
|
||||
dataRoutes: [],
|
||||
rewrites: framework.defaultRewrites ?? [],
|
||||
};
|
||||
await fs.writeJSON(
|
||||
join(cwd, OUTPUT_DIR, 'routes-manifest.json'),
|
||||
routesManifest,
|
||||
{ spaces: 2 }
|
||||
);
|
||||
}
|
||||
|
||||
// Special Next.js processing.
|
||||
if (isNextJs) {
|
||||
// The contents of `.output/static` should be placed inside of `.output/static/_next/static`
|
||||
const tempStatic = '___static';
|
||||
await fs.rename(
|
||||
join(cwd, OUTPUT_DIR, 'static'),
|
||||
join(cwd, OUTPUT_DIR, tempStatic)
|
||||
);
|
||||
await fs.mkdirp(join(cwd, OUTPUT_DIR, 'static', '_next', 'static'));
|
||||
await fs.rename(
|
||||
join(cwd, OUTPUT_DIR, tempStatic),
|
||||
join(cwd, OUTPUT_DIR, 'static', '_next', 'static')
|
||||
);
|
||||
|
||||
// Next.js might reference files from the `static` directory in `middleware-manifest.json`.
|
||||
// Since we move all files from `static` to `static/_next/static`, we'll need to change
|
||||
// those references as well and update the manifest file.
|
||||
const middlewareManifest = join(
|
||||
cwd,
|
||||
OUTPUT_DIR,
|
||||
'server',
|
||||
'middleware-manifest.json'
|
||||
);
|
||||
if (fs.existsSync(middlewareManifest)) {
|
||||
const manifest = await fs.readJSON(middlewareManifest);
|
||||
Object.keys(manifest.middleware).forEach(key => {
|
||||
const files = manifest.middleware[key].files.map((f: string) => {
|
||||
if (f.startsWith('static/')) {
|
||||
const next = f.replace(/^static\//gm, 'static/_next/static/');
|
||||
client.output.debug(
|
||||
`Replacing file in \`middleware-manifest.json\`: ${f} => ${next}`
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
return f;
|
||||
});
|
||||
|
||||
manifest.middleware[key].files = files;
|
||||
});
|
||||
|
||||
await fs.writeJSON(middlewareManifest, manifest);
|
||||
}
|
||||
|
||||
// We want to pick up directories for user-provided static files into `.`output/static`.
|
||||
// More specifically, the static directory contents would then be mounted to `output/static/static`,
|
||||
// and the public directory contents would be mounted to `output/static`. Old Next.js versions
|
||||
// allow `static`, and newer ones allow both, but since there's nobody that actually uses both,
|
||||
// we can check for the existence of both and pick the first match that we find (first
|
||||
// `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 publicFiles = await glob('public/**', {
|
||||
nodir: true,
|
||||
dot: true,
|
||||
cwd,
|
||||
absolute: true,
|
||||
});
|
||||
if (publicFiles.length > 0) {
|
||||
await Promise.all(
|
||||
publicFiles.map(f =>
|
||||
smartCopy(
|
||||
client,
|
||||
f,
|
||||
f.replace('public', join(OUTPUT_DIR, 'static'))
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const staticFiles = await glob('static/**', {
|
||||
nodir: true,
|
||||
dot: true,
|
||||
cwd,
|
||||
absolute: true,
|
||||
});
|
||||
await Promise.all(
|
||||
staticFiles.map(f =>
|
||||
smartCopy(
|
||||
client,
|
||||
f,
|
||||
f.replace('static', join(OUTPUT_DIR, 'static', 'static'))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Regardless of the Next.js version, we make sure that it is compatible with
|
||||
// the Filesystem API. We get there by moving all the files needed
|
||||
// into the outputs directory `inputs` folder. Next.js is > 12, we can
|
||||
// read the .nft.json files directly. If there aren't .nft.json files
|
||||
// we trace and create them. We then resolve the files in each nft file list
|
||||
// and move them into the "inputs" directory. We rename them with hashes to
|
||||
// prevent collisions and then update the related .nft files accordingly
|
||||
// to point to the newly named input files. Again, all of this is so that Next.js
|
||||
// works with the Filesystem API (and so .output contains all inputs
|
||||
// needed to run Next.js) and `vc --prebuilt`.
|
||||
const nftFiles = await glob(join(OUTPUT_DIR, '**', '*.nft.json'), {
|
||||
nodir: true,
|
||||
dot: true,
|
||||
cwd,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
// If there are no .nft.json files, we know that Next.js < 12. We then
|
||||
// execute the tracing on our own.
|
||||
if (nftFiles.length === 0) {
|
||||
const serverFiles = await glob(
|
||||
join(OUTPUT_DIR, 'server', 'pages', '**', '*.js'),
|
||||
{
|
||||
nodir: true,
|
||||
dot: true,
|
||||
cwd,
|
||||
ignore: ['webpack-runtime.js'],
|
||||
absolute: true,
|
||||
}
|
||||
);
|
||||
for (let f of serverFiles) {
|
||||
const { ext, dir } = parse(f);
|
||||
const { fileList } = await nodeFileTrace([f], {
|
||||
ignore: [
|
||||
relative(cwd, f),
|
||||
'node_modules/next/dist/pages/**/*',
|
||||
'node_modules/next/dist/compiled/webpack/(bundle4|bundle5).js',
|
||||
'node_modules/react/**/*.development.js',
|
||||
'node_modules/react-dom/**/*.development.js',
|
||||
'node_modules/use-subscription/**/*.development.js',
|
||||
'node_modules/sharp/**/*',
|
||||
],
|
||||
});
|
||||
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 requiredServerFilesPath = join(
|
||||
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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build Plugins
|
||||
if (plugins?.buildPlugins && plugins.buildPlugins.length > 0) {
|
||||
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') {
|
||||
const pluginStamp = stamp();
|
||||
const fullName = name + '.build';
|
||||
const prefix = chalk.gray(' > ') + color(fullName + ':');
|
||||
client.output.debug(`Running ${fullName}:`);
|
||||
try {
|
||||
console.log = (...args: any[]) => prefixedLog(prefix, args, origLog);
|
||||
console.error = (...args: any[]) =>
|
||||
prefixedLog(prefix, args, origErr);
|
||||
await plugin.build({
|
||||
vercelConfig,
|
||||
workPath: cwd,
|
||||
});
|
||||
client.output.debug(
|
||||
`Completed ${fullName} ${chalk.dim(`${pluginStamp()}`)}`
|
||||
);
|
||||
} catch (error) {
|
||||
client.output.error(`${prefix} failed`);
|
||||
handleError(error, { debug });
|
||||
return 1;
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
console.error = origLog;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${prependEmoji(
|
||||
`Build Completed in ${chalk.bold(OUTPUT_DIR)} ${chalk.gray(
|
||||
buildStamp()
|
||||
)}`,
|
||||
emoji('success')
|
||||
)}`
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runPackageJsonScript(
|
||||
client: Client,
|
||||
destPath: string,
|
||||
scriptNames: string | Iterable<string>,
|
||||
spawnOpts?: SpawnOptions
|
||||
) {
|
||||
assert(isAbsolute(destPath));
|
||||
|
||||
const { packageJson, cliType, lockfileVersion } = await scanParentDirs(
|
||||
destPath,
|
||||
true
|
||||
);
|
||||
const scriptName = getScriptName(
|
||||
packageJson,
|
||||
typeof scriptNames === 'string' ? [scriptNames] : scriptNames
|
||||
);
|
||||
if (!scriptName) return false;
|
||||
|
||||
client.output.debug('Running user script...');
|
||||
const runScriptTime = Date.now();
|
||||
|
||||
const opts: any = { cwd: destPath, ...spawnOpts };
|
||||
const env = (opts.env = { ...process.env, ...opts.env });
|
||||
|
||||
if (cliType === 'npm') {
|
||||
opts.prettyCommand = `npm run ${scriptName}`;
|
||||
|
||||
if (typeof lockfileVersion === 'number' && lockfileVersion >= 2) {
|
||||
// Ensure that npm 7 is at the beginning of the `$PATH`
|
||||
env.PATH = `/node16/bin-npm7:${env.PATH}`;
|
||||
}
|
||||
} else {
|
||||
opts.prettyCommand = `yarn run ${scriptName}`;
|
||||
|
||||
// Yarn v2 PnP mode may be activated, so force "node-modules" linker style
|
||||
if (!env.YARN_NODE_LINKER) {
|
||||
env.YARN_NODE_LINKER = 'node-modules';
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Running Build Command: ${cmd(opts.prettyCommand)}\n`);
|
||||
await spawnAsync(cliType, ['run', scriptName], opts);
|
||||
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 {
|
||||
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 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), 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.copy(existingPath, newPath, {
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function smartCopy(client: Client, from: string, to: string) {
|
||||
sema.acquire();
|
||||
try {
|
||||
client.output.debug(`Copying from ${from} to ${to}`);
|
||||
await linkOrCopy(from, to);
|
||||
} finally {
|
||||
sema.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function glob(pattern: string, options: GlobOptions): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ogGlob(pattern, options, (err, files) => {
|
||||
err ? reject(err) : resolve(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash for the given buf.
|
||||
*
|
||||
* @param {Buffer} file data
|
||||
* @return {String} hex digest
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Update the .nft.json with new input and output mapping
|
||||
await fs.writeJSON(nftFileName, {
|
||||
...nft,
|
||||
files: newFilesList,
|
||||
});
|
||||
}
|
||||
@@ -18,9 +18,10 @@ export const help = () => `
|
||||
init [example] Initialize an example project
|
||||
ls | list [app] Lists deployments
|
||||
inspect [id] Displays information related to a deployment
|
||||
link Link local directory to a Vercel Project
|
||||
link [path] Link local directory to a Vercel Project
|
||||
login [email] Logs into your account or creates a new one
|
||||
logout Logs out of your account
|
||||
pull [path] Pull your Project Settings from the cloud
|
||||
switch [scope] Switches between teams and your personal account
|
||||
help [cmd] Displays complete help for [cmd]
|
||||
|
||||
|
||||
@@ -1,14 +1,66 @@
|
||||
import ms from 'ms';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve, basename } from 'path';
|
||||
import { VercelConfig, fileNameSymbol } from '@vercel/client';
|
||||
import bytes from 'bytes';
|
||||
import chalk from 'chalk';
|
||||
import { join, resolve, basename } from 'path';
|
||||
import { Dictionary, fileNameSymbol, VercelConfig } from '@vercel/client';
|
||||
import code from '../../util/output/code';
|
||||
import highlight from '../../util/output/highlight';
|
||||
import { readLocalConfig } from '../../util/config/files';
|
||||
import getArgs from '../../util/get-args';
|
||||
import { handleError } from '../../util/error';
|
||||
import { help } from './args';
|
||||
import deploy from './latest';
|
||||
import Client from '../../util/client';
|
||||
import { write as copy } from 'clipboardy';
|
||||
import { getPrettyError } from '@vercel/build-utils';
|
||||
import toHumanPath from '../../util/humanize-path';
|
||||
import Now from '../../util';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import createDeploy from '../../util/deploy/create-deploy';
|
||||
import getDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host';
|
||||
import parseMeta from '../../util/parse-meta';
|
||||
import linkStyle from '../../util/output/link';
|
||||
import param from '../../util/output/param';
|
||||
import {
|
||||
BuildsRateLimited,
|
||||
DeploymentNotFound,
|
||||
DeploymentPermissionDenied,
|
||||
InvalidDeploymentId,
|
||||
DomainNotFound,
|
||||
DomainNotVerified,
|
||||
DomainPermissionDenied,
|
||||
DomainVerificationFailed,
|
||||
InvalidDomain,
|
||||
TooManyRequests,
|
||||
UserAborted,
|
||||
DeploymentsRateLimited,
|
||||
AliasDomainConfigured,
|
||||
MissingBuildScript,
|
||||
ConflictingFilePath,
|
||||
ConflictingPathSegment,
|
||||
BuildError,
|
||||
NotDomainOwner,
|
||||
} from '../../util/errors-ts';
|
||||
import { SchemaValidationFailed } from '../../util/errors';
|
||||
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import editProjectSettings from '../../util/input/edit-project-settings';
|
||||
import {
|
||||
getLinkedProject,
|
||||
linkFolderToProject,
|
||||
} from '../../util/projects/link';
|
||||
import getProjectName from '../../util/get-project-name';
|
||||
import selectOrg from '../../util/input/select-org';
|
||||
import inputProject from '../../util/input/input-project';
|
||||
import { prependEmoji, emoji } from '../../util/emoji';
|
||||
import { inputRootDirectory } from '../../util/input/input-root-directory';
|
||||
import validatePaths, {
|
||||
validateRootDirectory,
|
||||
} from '../../util/validate-paths';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { getPreferredPreviewURL } from '../../util/deploy/get-preferred-preview-url';
|
||||
import { Output } from '../../util/output';
|
||||
import { help } from './args';
|
||||
import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks';
|
||||
|
||||
export default async (client: Client) => {
|
||||
const { output } = client;
|
||||
@@ -27,6 +79,7 @@ export default async (client: Client) => {
|
||||
// This is not an array in favor of matching
|
||||
// the config property name.
|
||||
'--regions': String,
|
||||
'--prebuilt': Boolean,
|
||||
'--prod': Boolean,
|
||||
'--confirm': Boolean,
|
||||
'-f': '--force',
|
||||
@@ -65,10 +118,7 @@ export default async (client: Client) => {
|
||||
paths = [process.cwd()];
|
||||
}
|
||||
|
||||
let localConfig: VercelConfig | null = client.localConfig;
|
||||
if (!localConfig || localConfig instanceof Error) {
|
||||
localConfig = readLocalConfig(paths[0]);
|
||||
}
|
||||
let localConfig = client.localConfig || readLocalConfig(paths[0]);
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
@@ -105,5 +155,755 @@ export default async (client: Client) => {
|
||||
}
|
||||
}
|
||||
|
||||
return deploy(client, paths, localConfig, argv);
|
||||
const { log, debug, error, warn, isTTY } = output;
|
||||
|
||||
const quiet = !isTTY;
|
||||
|
||||
// check paths
|
||||
const pathValidation = await validatePaths(output, paths);
|
||||
|
||||
if (!pathValidation.valid) {
|
||||
return pathValidation.exitCode;
|
||||
}
|
||||
|
||||
const { isFile, path } = pathValidation;
|
||||
const autoConfirm = argv['--confirm'] || isFile;
|
||||
|
||||
// deprecate --name
|
||||
if (argv['--name']) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`The ${param(
|
||||
'--name'
|
||||
)} option is deprecated (https://vercel.link/name-flag)`,
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
// retrieve `project` and `org` from .vercel
|
||||
const link = await getLinkedProject(client, path);
|
||||
|
||||
if (link.status === 'error') {
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
let { org, project, status } = link;
|
||||
|
||||
let newProjectName = null;
|
||||
let rootDirectory = project ? project.rootDirectory : null;
|
||||
let sourceFilesOutsideRootDirectory = true;
|
||||
|
||||
if (status === 'not_linked') {
|
||||
const shouldStartSetup =
|
||||
autoConfirm ||
|
||||
(await confirm(
|
||||
`Set up and deploy ${chalk.cyan(`“${toHumanPath(path)}”`)}?`,
|
||||
true
|
||||
));
|
||||
|
||||
if (!shouldStartSetup) {
|
||||
output.print(`Aborted. Project not set up.\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
org = await selectOrg(
|
||||
client,
|
||||
'Which scope do you want to deploy to?',
|
||||
autoConfirm
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
// We use `localConfig` here to read the name
|
||||
// even though the `vercel.json` file can change
|
||||
// afterwards, this is fine since the property
|
||||
// will be deprecated and can be replaced with
|
||||
// user input.
|
||||
const detectedProjectName = getProjectName({
|
||||
argv,
|
||||
nowConfig: localConfig || {},
|
||||
isFile,
|
||||
paths,
|
||||
});
|
||||
|
||||
const projectOrNewProjectName = await inputProject(
|
||||
client,
|
||||
org,
|
||||
detectedProjectName,
|
||||
autoConfirm
|
||||
);
|
||||
|
||||
if (typeof projectOrNewProjectName === 'string') {
|
||||
newProjectName = projectOrNewProjectName;
|
||||
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
|
||||
} else {
|
||||
project = projectOrNewProjectName;
|
||||
rootDirectory = project.rootDirectory;
|
||||
sourceFilesOutsideRootDirectory = project.sourceFilesOutsideRootDirectory;
|
||||
|
||||
// we can already link the project
|
||||
await linkFolderToProject(
|
||||
output,
|
||||
path,
|
||||
{
|
||||
projectId: project.id,
|
||||
orgId: org.id,
|
||||
},
|
||||
project.name,
|
||||
org.slug
|
||||
);
|
||||
status = 'linked';
|
||||
}
|
||||
}
|
||||
|
||||
// At this point `org` should be populated
|
||||
if (!org) {
|
||||
throw new Error(`"org" is not defined`);
|
||||
}
|
||||
|
||||
// Set the `contextName` and `currentTeam` as specified by the
|
||||
// Project Settings, so that API calls happen with the proper scope
|
||||
const contextName = org.slug;
|
||||
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
|
||||
// if we have `sourceFilesOutsideRootDirectory` set to `true`, we use the current path
|
||||
// and upload the entire directory.
|
||||
const sourcePath =
|
||||
rootDirectory && !sourceFilesOutsideRootDirectory
|
||||
? join(path, rootDirectory)
|
||||
: path;
|
||||
|
||||
if (
|
||||
rootDirectory &&
|
||||
(await validateRootDirectory(
|
||||
output,
|
||||
path,
|
||||
sourcePath,
|
||||
project
|
||||
? `To change your Project Settings, go to https://vercel.com/${org?.slug}/${project.name}/settings`
|
||||
: ''
|
||||
)) === false
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If Root Directory is used we'll try to read the config
|
||||
// from there instead and use it if it exists.
|
||||
if (rootDirectory) {
|
||||
const rootDirectoryConfig = readLocalConfig(join(path, rootDirectory));
|
||||
|
||||
if (rootDirectoryConfig) {
|
||||
debug(`Read local config from root directory (${rootDirectory})`);
|
||||
localConfig = rootDirectoryConfig;
|
||||
} else if (localConfig) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`The ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} file should be inside of the provided root directory.`,
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
localConfig = localConfig || {};
|
||||
|
||||
if (localConfig.name) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`The ${code('name')} property in ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} is deprecated (https://vercel.link/name-prop)`,
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
// build `env`
|
||||
const isObject = (item: any) =>
|
||||
Object.prototype.toString.call(item) === '[object Object]';
|
||||
|
||||
// This validation needs to happen on the client side because
|
||||
// the data is merged with other data before it is passed to the API (which
|
||||
// also does schema validation).
|
||||
if (typeof localConfig.env !== 'undefined' && !isObject(localConfig.env)) {
|
||||
error(
|
||||
`The ${code('env')} property in ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} needs to be an object`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (typeof localConfig.build !== 'undefined') {
|
||||
if (!isObject(localConfig.build)) {
|
||||
error(
|
||||
`The ${code('build')} property in ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} needs to be an object`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof localConfig.build.env !== 'undefined' &&
|
||||
!isObject(localConfig.build.env)
|
||||
) {
|
||||
error(
|
||||
`The ${code('build.env')} property in ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} needs to be an object`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// build `meta`
|
||||
const meta = Object.assign(
|
||||
{},
|
||||
parseMeta(localConfig.meta),
|
||||
parseMeta(argv['--meta'])
|
||||
);
|
||||
|
||||
// Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments
|
||||
const deploymentEnv = Object.assign(
|
||||
{},
|
||||
parseEnv(localConfig.env),
|
||||
parseEnv(argv['--env'])
|
||||
);
|
||||
|
||||
// Merge build env out of `build.env` from vercel.json, and `--build-env` args
|
||||
const deploymentBuildEnv = Object.assign(
|
||||
{},
|
||||
parseEnv(localConfig.build && localConfig.build.env),
|
||||
parseEnv(argv['--build-env'])
|
||||
);
|
||||
|
||||
// If there's any undefined values, then inherit them from this process
|
||||
try {
|
||||
await addProcessEnv(log, deploymentEnv);
|
||||
await addProcessEnv(log, deploymentBuildEnv);
|
||||
} catch (err) {
|
||||
error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// build `regions`
|
||||
const regionFlag = (argv['--regions'] || '')
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean);
|
||||
const regions = regionFlag.length > 0 ? regionFlag : localConfig.regions;
|
||||
|
||||
// build `target`
|
||||
let target;
|
||||
if (argv['--target']) {
|
||||
const deprecatedTarget = argv['--target'];
|
||||
|
||||
if (!['staging', 'production'].includes(deprecatedTarget)) {
|
||||
error(
|
||||
`The specified ${param('--target')} ${code(
|
||||
deprecatedTarget
|
||||
)} is not valid`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deprecatedTarget === 'production') {
|
||||
warn(
|
||||
'We recommend using the much shorter `--prod` option instead of `--target production` (deprecated)'
|
||||
);
|
||||
}
|
||||
|
||||
output.debug(`Setting target to ${deprecatedTarget}`);
|
||||
target = deprecatedTarget;
|
||||
} else if (argv['--prod']) {
|
||||
output.debug('Setting target to production');
|
||||
target = 'production';
|
||||
}
|
||||
|
||||
const currentTeam = org?.type === 'team' ? org.id : undefined;
|
||||
const now = new Now({
|
||||
client,
|
||||
currentTeam,
|
||||
});
|
||||
let deployStamp = stamp();
|
||||
let deployment = null;
|
||||
|
||||
try {
|
||||
const createArgs: any = {
|
||||
name: project ? project.name : newProjectName,
|
||||
env: deploymentEnv,
|
||||
build: { env: deploymentBuildEnv },
|
||||
forceNew: argv['--force'],
|
||||
withCache: argv['--with-cache'],
|
||||
prebuilt: argv['--prebuilt'],
|
||||
quiet,
|
||||
wantsPublic: argv['--public'] || localConfig.public,
|
||||
isFile,
|
||||
type: null,
|
||||
nowConfig: localConfig,
|
||||
regions,
|
||||
meta,
|
||||
deployStamp,
|
||||
target,
|
||||
skipAutoDetectionConfirmation: autoConfirm,
|
||||
};
|
||||
|
||||
if (!localConfig.builds || localConfig.builds.length === 0) {
|
||||
// Only add projectSettings for zero config deployments
|
||||
createArgs.projectSettings = { sourceFilesOutsideRootDirectory };
|
||||
}
|
||||
|
||||
deployment = await createDeploy(
|
||||
client,
|
||||
now,
|
||||
contextName,
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
!project && !isFile,
|
||||
path
|
||||
);
|
||||
|
||||
if (deployment.code === 'missing_project_settings') {
|
||||
let { projectSettings, framework } = deployment;
|
||||
if (rootDirectory) {
|
||||
projectSettings.rootDirectory = rootDirectory;
|
||||
}
|
||||
|
||||
if (typeof sourceFilesOutsideRootDirectory !== 'undefined') {
|
||||
projectSettings.sourceFilesOutsideRootDirectory =
|
||||
sourceFilesOutsideRootDirectory;
|
||||
}
|
||||
|
||||
const settings = await editProjectSettings(
|
||||
output,
|
||||
projectSettings,
|
||||
framework
|
||||
);
|
||||
|
||||
// deploy again, but send projectSettings this time
|
||||
createArgs.projectSettings = settings;
|
||||
|
||||
deployStamp = stamp();
|
||||
createArgs.deployStamp = deployStamp;
|
||||
deployment = await createDeploy(
|
||||
client,
|
||||
now,
|
||||
contextName,
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
false,
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
if (deployment instanceof NotDomainOwner) {
|
||||
output.error(deployment.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deployment instanceof Error) {
|
||||
output.error(
|
||||
deployment.message ||
|
||||
'An unexpected error occurred while deploying your project',
|
||||
undefined,
|
||||
'https://vercel.link/help',
|
||||
'Contact Support'
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deployment.readyState === 'CANCELED') {
|
||||
output.print('The deployment has been canceled.\n');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deployment.checksConclusion === 'failed') {
|
||||
const { checks } = await getDeploymentChecks(client, deployment.id);
|
||||
const counters = new Map<string, number>();
|
||||
checks.forEach(c => {
|
||||
counters.set(c.conclusion, (counters.get(c.conclusion) ?? 0) + 1);
|
||||
});
|
||||
|
||||
const counterList = Array.from(counters)
|
||||
.map(([name, no]) => `${no} ${name}`)
|
||||
.join(', ');
|
||||
output.error(`Running Checks: ${counterList}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const deploymentResponse = await getDeploymentByIdOrHost(
|
||||
client,
|
||||
contextName,
|
||||
deployment.id,
|
||||
'v10'
|
||||
);
|
||||
|
||||
if (
|
||||
deploymentResponse instanceof DeploymentNotFound ||
|
||||
deploymentResponse instanceof DeploymentPermissionDenied ||
|
||||
deploymentResponse instanceof InvalidDeploymentId
|
||||
) {
|
||||
output.error(deploymentResponse.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deployment === null) {
|
||||
error('Uploading failed. Please try again.');
|
||||
return 1;
|
||||
}
|
||||
} catch (err) {
|
||||
debug(`Error: ${err}\n${err.stack}`);
|
||||
|
||||
if (err instanceof NotDomainOwner) {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err instanceof DomainNotFound && err.meta && err.meta.domain) {
|
||||
output.debug(
|
||||
`The domain ${err.meta.domain} was not found, trying to purchase it`
|
||||
);
|
||||
|
||||
const purchase = await purchaseDomainIfAvailable(
|
||||
output,
|
||||
client,
|
||||
err.meta.domain,
|
||||
contextName
|
||||
);
|
||||
|
||||
if (purchase === true) {
|
||||
output.success(`Successfully purchased the domain ${err.meta.domain}!`);
|
||||
|
||||
// We exit if the purchase is completed since
|
||||
// the domain verification can take some time
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (purchase === false || purchase instanceof UserAborted) {
|
||||
handleCreateDeployError(output, deployment, localConfig);
|
||||
return 1;
|
||||
}
|
||||
|
||||
handleCreateDeployError(output, purchase, localConfig);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof DomainNotFound ||
|
||||
err instanceof DomainNotVerified ||
|
||||
err instanceof NotDomainOwner ||
|
||||
err instanceof DomainPermissionDenied ||
|
||||
err instanceof DomainVerificationFailed ||
|
||||
err instanceof SchemaValidationFailed ||
|
||||
err instanceof InvalidDomain ||
|
||||
err instanceof DeploymentNotFound ||
|
||||
err instanceof BuildsRateLimited ||
|
||||
err instanceof DeploymentsRateLimited ||
|
||||
err instanceof AliasDomainConfigured ||
|
||||
err instanceof MissingBuildScript ||
|
||||
err instanceof ConflictingFilePath ||
|
||||
err instanceof ConflictingPathSegment
|
||||
) {
|
||||
handleCreateDeployError(output, err, localConfig);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err instanceof BuildError) {
|
||||
output.error(err.message || 'Build failed');
|
||||
output.error(
|
||||
`Check your logs at https://${now.url}/_logs or run ${getCommandName(
|
||||
`logs ${now.url}`
|
||||
)}`
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err.keyword === 'additionalProperties' && err.dataPath === '.scale') {
|
||||
const { additionalProperty = '' } = err.params || {};
|
||||
const message = `Invalid DC name for the scale option: ${additionalProperty}`;
|
||||
error(message);
|
||||
}
|
||||
|
||||
if (err.code === 'size_limit_exceeded') {
|
||||
const { sizeLimit = 0 } = err;
|
||||
const message = `File size limit exceeded (${bytes(sizeLimit)})`;
|
||||
error(message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
handleError(err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return printDeploymentStatus(
|
||||
output,
|
||||
client,
|
||||
deployment,
|
||||
deployStamp,
|
||||
!argv['--no-clipboard'],
|
||||
isFile
|
||||
);
|
||||
};
|
||||
|
||||
function handleCreateDeployError(
|
||||
output: Output,
|
||||
error: Error,
|
||||
localConfig: VercelConfig
|
||||
) {
|
||||
if (error instanceof InvalidDomain) {
|
||||
output.error(`The domain ${error.meta.domain} is not valid`);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof DomainVerificationFailed) {
|
||||
output.error(
|
||||
`The domain used as a suffix ${chalk.underline(
|
||||
error.meta.domain
|
||||
)} is not verified and can't be used as custom suffix.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof DomainPermissionDenied) {
|
||||
output.error(
|
||||
`You don't have permissions to access the domain used as a suffix ${chalk.underline(
|
||||
error.meta.domain
|
||||
)}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof SchemaValidationFailed) {
|
||||
const niceError = getPrettyError(error.meta);
|
||||
const fileName = localConfig[fileNameSymbol] || 'vercel.json';
|
||||
niceError.message = `Invalid ${fileName} - ${niceError.message}`;
|
||||
output.prettyError(niceError);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof TooManyRequests) {
|
||||
output.error(
|
||||
`Too many requests detected for ${error.meta.api} API. Try again in ${ms(
|
||||
error.meta.retryAfter * 1000,
|
||||
{
|
||||
long: true,
|
||||
}
|
||||
)}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof DomainNotVerified) {
|
||||
output.error(
|
||||
`The domain used as an alias ${chalk.underline(
|
||||
error.meta.domain
|
||||
)} is not verified yet. Please verify it.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof BuildsRateLimited) {
|
||||
output.error(error.message);
|
||||
output.note(
|
||||
`Run ${getCommandName('upgrade')} to increase your builds limit.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (
|
||||
error instanceof DeploymentNotFound ||
|
||||
error instanceof NotDomainOwner ||
|
||||
error instanceof DeploymentsRateLimited ||
|
||||
error instanceof AliasDomainConfigured ||
|
||||
error instanceof MissingBuildScript ||
|
||||
error instanceof ConflictingFilePath ||
|
||||
error instanceof ConflictingPathSegment
|
||||
) {
|
||||
output.error(error.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
const addProcessEnv = async (
|
||||
log: (str: string) => void,
|
||||
env: typeof process.env
|
||||
) => {
|
||||
let val;
|
||||
|
||||
for (const key of Object.keys(env)) {
|
||||
if (typeof env[key] !== 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
val = process.env[key];
|
||||
|
||||
if (typeof val === 'string') {
|
||||
log(
|
||||
`Reading ${chalk.bold(
|
||||
`"${chalk.bold(key)}"`
|
||||
)} from your env (as no value was specified)`
|
||||
);
|
||||
// Escape value if it begins with @
|
||||
env[key] = val.replace(/^@/, '\\@');
|
||||
} else {
|
||||
throw new Error(
|
||||
`No value specified for env ${chalk.bold(
|
||||
`"${chalk.bold(key)}"`
|
||||
)} and it was not found in your env.`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const printDeploymentStatus = async (
|
||||
output: Output,
|
||||
client: Client,
|
||||
{
|
||||
readyState,
|
||||
alias: aliasList,
|
||||
aliasError,
|
||||
target,
|
||||
indications,
|
||||
url: deploymentUrl,
|
||||
aliasWarning,
|
||||
}: {
|
||||
readyState: string;
|
||||
alias: string[];
|
||||
aliasError: Error;
|
||||
target: string;
|
||||
indications: any;
|
||||
url: string;
|
||||
aliasWarning?: {
|
||||
code: string;
|
||||
message: string;
|
||||
link?: string;
|
||||
action?: string;
|
||||
};
|
||||
},
|
||||
deployStamp: () => string,
|
||||
isClipboardEnabled: boolean,
|
||||
isFile: boolean
|
||||
) => {
|
||||
indications = indications || [];
|
||||
const isProdDeployment = target === 'production';
|
||||
|
||||
if (readyState !== 'READY') {
|
||||
output.error(
|
||||
`Your deployment failed. Please retry later. More: https://err.sh/vercel/deployment-error`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aliasError) {
|
||||
output.warn(
|
||||
`Failed to assign aliases${
|
||||
aliasError.message ? `: ${aliasError.message}` : ''
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
// print preview/production url
|
||||
let previewUrl: string;
|
||||
let isWildcard: boolean;
|
||||
if (!isFile && Array.isArray(aliasList) && aliasList.length > 0) {
|
||||
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
|
||||
if (previewUrlInfo) {
|
||||
isWildcard = previewUrlInfo.isWildcard;
|
||||
previewUrl = previewUrlInfo.previewUrl;
|
||||
} else {
|
||||
isWildcard = false;
|
||||
previewUrl = `https://${deploymentUrl}`;
|
||||
}
|
||||
} else {
|
||||
// fallback to deployment url
|
||||
isWildcard = false;
|
||||
previewUrl = `https://${deploymentUrl}`;
|
||||
}
|
||||
|
||||
// copy to clipboard
|
||||
let isCopiedToClipboard = false;
|
||||
if (isClipboardEnabled && !isWildcard) {
|
||||
try {
|
||||
await copy(previewUrl);
|
||||
isCopiedToClipboard = true;
|
||||
} catch (err) {
|
||||
output.debug(`Error copyind to clipboard: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
output.print(
|
||||
prependEmoji(
|
||||
`${isProdDeployment ? 'Production' : 'Preview'}: ${chalk.bold(
|
||||
previewUrl
|
||||
)}${
|
||||
isCopiedToClipboard ? chalk.gray(` [copied to clipboard]`) : ''
|
||||
} ${deployStamp()}`,
|
||||
emoji('success')
|
||||
) + `\n`
|
||||
);
|
||||
}
|
||||
|
||||
if (aliasWarning?.message) {
|
||||
indications.push({
|
||||
type: 'warning',
|
||||
payload: aliasWarning.message,
|
||||
link: aliasWarning.link,
|
||||
action: aliasWarning.action,
|
||||
});
|
||||
}
|
||||
|
||||
const newline = '\n';
|
||||
for (let indication of indications) {
|
||||
const message =
|
||||
prependEmoji(chalk.dim(indication.payload), emoji(indication.type)) +
|
||||
newline;
|
||||
let link = '';
|
||||
if (indication.link)
|
||||
link =
|
||||
chalk.dim(
|
||||
`${indication.action || 'Learn More'}: ${linkStyle(indication.link)}`
|
||||
) + newline;
|
||||
output.print(message + link);
|
||||
}
|
||||
};
|
||||
|
||||
// Converts `env` Arrays, Strings and Objects into env Objects.
|
||||
const parseEnv = (env?: string[] | Dictionary<string>) => {
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof env === 'string') {
|
||||
// a single `--env` arg comes in as a String
|
||||
env = [env];
|
||||
}
|
||||
|
||||
if (Array.isArray(env)) {
|
||||
return env.reduce((o, e) => {
|
||||
let key;
|
||||
let value;
|
||||
const equalsSign = e.indexOf('=');
|
||||
|
||||
if (equalsSign === -1) {
|
||||
key = e;
|
||||
} else {
|
||||
key = e.substr(0, equalsSign);
|
||||
value = e.substr(equalsSign + 1);
|
||||
}
|
||||
|
||||
o[key] = value;
|
||||
return o;
|
||||
}, {} as Dictionary<string | undefined>);
|
||||
}
|
||||
|
||||
// assume it's already an Object
|
||||
return env;
|
||||
};
|
||||
|
||||
@@ -1,808 +0,0 @@
|
||||
import ms from 'ms';
|
||||
import bytes from 'bytes';
|
||||
import { join } from 'path';
|
||||
import { write as copy } from 'clipboardy';
|
||||
import chalk from 'chalk';
|
||||
import { Dictionary, fileNameSymbol, VercelConfig } from '@vercel/client';
|
||||
import { getPrettyError } from '@vercel/build-utils';
|
||||
import { handleError } from '../../util/error';
|
||||
import toHumanPath from '../../util/humanize-path';
|
||||
import Now from '../../util';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import createDeploy from '../../util/deploy/create-deploy';
|
||||
import getDeploymentByIdOrHost from '../../util/deploy/get-deployment-by-id-or-host';
|
||||
import parseMeta from '../../util/parse-meta';
|
||||
import code from '../../util/output/code';
|
||||
import linkStyle from '../../util/output/link';
|
||||
import param from '../../util/output/param';
|
||||
import highlight from '../../util/output/highlight';
|
||||
import {
|
||||
BuildsRateLimited,
|
||||
DeploymentNotFound,
|
||||
DeploymentPermissionDenied,
|
||||
InvalidDeploymentId,
|
||||
DomainNotFound,
|
||||
DomainNotVerified,
|
||||
DomainPermissionDenied,
|
||||
DomainVerificationFailed,
|
||||
InvalidDomain,
|
||||
TooManyRequests,
|
||||
UserAborted,
|
||||
DeploymentsRateLimited,
|
||||
AliasDomainConfigured,
|
||||
MissingBuildScript,
|
||||
ConflictingFilePath,
|
||||
ConflictingPathSegment,
|
||||
BuildError,
|
||||
NotDomainOwner,
|
||||
} from '../../util/errors-ts';
|
||||
import { SchemaValidationFailed } from '../../util/errors';
|
||||
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import editProjectSettings from '../../util/input/edit-project-settings';
|
||||
import {
|
||||
getLinkedProject,
|
||||
linkFolderToProject,
|
||||
} from '../../util/projects/link';
|
||||
import getProjectName from '../../util/get-project-name';
|
||||
import selectOrg from '../../util/input/select-org';
|
||||
import inputProject from '../../util/input/input-project';
|
||||
import { prependEmoji, emoji } from '../../util/emoji';
|
||||
import { inputRootDirectory } from '../../util/input/input-root-directory';
|
||||
import validatePaths, {
|
||||
validateRootDirectory,
|
||||
} from '../../util/validate-paths';
|
||||
import { readLocalConfig } from '../../util/config/files';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { getPreferredPreviewURL } from '../../util/deploy/get-preferred-preview-url';
|
||||
import { Output } from '../../util/output';
|
||||
import Client from '../../util/client';
|
||||
|
||||
const addProcessEnv = async (
|
||||
log: (str: string) => void,
|
||||
env: typeof process.env
|
||||
) => {
|
||||
let val;
|
||||
|
||||
for (const key of Object.keys(env)) {
|
||||
if (typeof env[key] !== 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
val = process.env[key];
|
||||
|
||||
if (typeof val === 'string') {
|
||||
log(
|
||||
`Reading ${chalk.bold(
|
||||
`"${chalk.bold(key)}"`
|
||||
)} from your env (as no value was specified)`
|
||||
);
|
||||
// Escape value if it begins with @
|
||||
env[key] = val.replace(/^@/, '\\@');
|
||||
} else {
|
||||
throw new Error(
|
||||
`No value specified for env ${chalk.bold(
|
||||
`"${chalk.bold(key)}"`
|
||||
)} and it was not found in your env.`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const printDeploymentStatus = async (
|
||||
output: Output,
|
||||
client: Client,
|
||||
{
|
||||
readyState,
|
||||
alias: aliasList,
|
||||
aliasError,
|
||||
target,
|
||||
indications,
|
||||
url: deploymentUrl,
|
||||
aliasWarning,
|
||||
}: {
|
||||
readyState: string;
|
||||
alias: string[];
|
||||
aliasError: Error;
|
||||
target: string;
|
||||
indications: any;
|
||||
url: string;
|
||||
aliasWarning?: {
|
||||
code: string;
|
||||
message: string;
|
||||
link?: string;
|
||||
action?: string;
|
||||
};
|
||||
},
|
||||
deployStamp: () => string,
|
||||
isClipboardEnabled: boolean,
|
||||
isFile: boolean
|
||||
) => {
|
||||
indications = indications || [];
|
||||
const isProdDeployment = target === 'production';
|
||||
|
||||
if (readyState !== 'READY') {
|
||||
output.error(
|
||||
`Your deployment failed. Please retry later. More: https://err.sh/vercel/deployment-error`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aliasError) {
|
||||
output.warn(
|
||||
`Failed to assign aliases${
|
||||
aliasError.message ? `: ${aliasError.message}` : ''
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
// print preview/production url
|
||||
let previewUrl: string;
|
||||
let isWildcard: boolean;
|
||||
if (!isFile && Array.isArray(aliasList) && aliasList.length > 0) {
|
||||
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
|
||||
if (previewUrlInfo) {
|
||||
isWildcard = previewUrlInfo.isWildcard;
|
||||
previewUrl = previewUrlInfo.previewUrl;
|
||||
} else {
|
||||
isWildcard = false;
|
||||
previewUrl = `https://${deploymentUrl}`;
|
||||
}
|
||||
} else {
|
||||
// fallback to deployment url
|
||||
isWildcard = false;
|
||||
previewUrl = `https://${deploymentUrl}`;
|
||||
}
|
||||
|
||||
// copy to clipboard
|
||||
let isCopiedToClipboard = false;
|
||||
if (isClipboardEnabled && !isWildcard) {
|
||||
try {
|
||||
await copy(previewUrl);
|
||||
isCopiedToClipboard = true;
|
||||
} catch (err) {
|
||||
output.debug(`Error copyind to clipboard: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
output.print(
|
||||
prependEmoji(
|
||||
`${isProdDeployment ? 'Production' : 'Preview'}: ${chalk.bold(
|
||||
previewUrl
|
||||
)}${
|
||||
isCopiedToClipboard ? chalk.gray(` [copied to clipboard]`) : ''
|
||||
} ${deployStamp()}`,
|
||||
emoji('success')
|
||||
) + `\n`
|
||||
);
|
||||
}
|
||||
|
||||
if (aliasWarning?.message) {
|
||||
indications.push({
|
||||
type: 'warning',
|
||||
payload: aliasWarning.message,
|
||||
link: aliasWarning.link,
|
||||
action: aliasWarning.action,
|
||||
});
|
||||
}
|
||||
|
||||
const newline = '\n';
|
||||
for (let indication of indications) {
|
||||
const message =
|
||||
prependEmoji(chalk.dim(indication.payload), emoji(indication.type)) +
|
||||
newline;
|
||||
let link = '';
|
||||
if (indication.link)
|
||||
link =
|
||||
chalk.dim(
|
||||
`${indication.action || 'Learn More'}: ${linkStyle(indication.link)}`
|
||||
) + newline;
|
||||
output.print(message + link);
|
||||
}
|
||||
};
|
||||
|
||||
// Converts `env` Arrays, Strings and Objects into env Objects.
|
||||
const parseEnv = (env?: string[] | Dictionary<string>) => {
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof env === 'string') {
|
||||
// a single `--env` arg comes in as a String
|
||||
env = [env];
|
||||
}
|
||||
|
||||
if (Array.isArray(env)) {
|
||||
return env.reduce((o, e) => {
|
||||
let key;
|
||||
let value;
|
||||
const equalsSign = e.indexOf('=');
|
||||
|
||||
if (equalsSign === -1) {
|
||||
key = e;
|
||||
} else {
|
||||
key = e.substr(0, equalsSign);
|
||||
value = e.substr(equalsSign + 1);
|
||||
}
|
||||
|
||||
o[key] = value;
|
||||
return o;
|
||||
}, {} as Dictionary<string | undefined>);
|
||||
}
|
||||
|
||||
// assume it's already an Object
|
||||
return env;
|
||||
};
|
||||
|
||||
export default async function main(
|
||||
client: Client,
|
||||
paths: string[],
|
||||
localConfig: VercelConfig | null,
|
||||
argv: any
|
||||
) {
|
||||
const {
|
||||
apiUrl,
|
||||
output,
|
||||
authConfig: { token },
|
||||
} = client;
|
||||
const { log, debug, error, warn } = output;
|
||||
const debugEnabled = argv['--debug'];
|
||||
|
||||
const { isTTY } = process.stdout;
|
||||
const quiet = !isTTY;
|
||||
|
||||
// check paths
|
||||
const pathValidation = await validatePaths(output, paths);
|
||||
|
||||
if (!pathValidation.valid) {
|
||||
return pathValidation.exitCode;
|
||||
}
|
||||
|
||||
const { isFile, path } = pathValidation;
|
||||
const autoConfirm = argv['--confirm'] || isFile;
|
||||
|
||||
// deprecate --name
|
||||
if (argv['--name']) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`The ${param(
|
||||
'--name'
|
||||
)} option is deprecated (https://vercel.link/name-flag)`,
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
// retrieve `project` and `org` from .vercel
|
||||
const link = await getLinkedProject(client, path);
|
||||
|
||||
if (link.status === 'error') {
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
let { org, project, status } = link;
|
||||
|
||||
let newProjectName = null;
|
||||
let rootDirectory = project ? project.rootDirectory : null;
|
||||
let sourceFilesOutsideRootDirectory = true;
|
||||
|
||||
if (status === 'not_linked') {
|
||||
const shouldStartSetup =
|
||||
autoConfirm ||
|
||||
(await confirm(
|
||||
`Set up and deploy ${chalk.cyan(`“${toHumanPath(path)}”`)}?`,
|
||||
true
|
||||
));
|
||||
|
||||
if (!shouldStartSetup) {
|
||||
output.print(`Aborted. Project not set up.\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
org = await selectOrg(
|
||||
client,
|
||||
'Which scope do you want to deploy to?',
|
||||
autoConfirm
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
// We use `localConfig` here to read the name
|
||||
// even though the `vercel.json` file can change
|
||||
// afterwards, this is fine since the property
|
||||
// will be deprecated and can be replaced with
|
||||
// user input.
|
||||
const detectedProjectName = getProjectName({
|
||||
argv,
|
||||
nowConfig: localConfig || {},
|
||||
isFile,
|
||||
paths,
|
||||
});
|
||||
|
||||
const projectOrNewProjectName = await inputProject(
|
||||
output,
|
||||
client,
|
||||
org,
|
||||
detectedProjectName,
|
||||
autoConfirm
|
||||
);
|
||||
|
||||
if (typeof projectOrNewProjectName === 'string') {
|
||||
newProjectName = projectOrNewProjectName;
|
||||
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
|
||||
} else {
|
||||
project = projectOrNewProjectName;
|
||||
rootDirectory = project.rootDirectory;
|
||||
sourceFilesOutsideRootDirectory = project.sourceFilesOutsideRootDirectory;
|
||||
|
||||
// we can already link the project
|
||||
await linkFolderToProject(
|
||||
output,
|
||||
path,
|
||||
{
|
||||
projectId: project.id,
|
||||
orgId: org.id,
|
||||
},
|
||||
project.name,
|
||||
org.slug
|
||||
);
|
||||
status = 'linked';
|
||||
}
|
||||
}
|
||||
|
||||
// At this point `org` should be populated
|
||||
if (!org) {
|
||||
throw new Error(`"org" is not defined`);
|
||||
}
|
||||
|
||||
// Set the `contextName` and `currentTeam` as specified by the
|
||||
// Project Settings, so that API calls happen with the proper scope
|
||||
const contextName = org.slug;
|
||||
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
|
||||
// if we have `sourceFilesOutsideRootDirectory` set to `true`, we use the current path
|
||||
// and upload the entire directory.
|
||||
const sourcePath =
|
||||
rootDirectory && !sourceFilesOutsideRootDirectory
|
||||
? join(path, rootDirectory)
|
||||
: path;
|
||||
|
||||
if (
|
||||
rootDirectory &&
|
||||
(await validateRootDirectory(
|
||||
output,
|
||||
path,
|
||||
sourcePath,
|
||||
project
|
||||
? `To change your Project Settings, go to https://vercel.com/${org?.slug}/${project.name}/settings`
|
||||
: ''
|
||||
)) === false
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If Root Directory is used we'll try to read the config
|
||||
// from there instead and use it if it exists.
|
||||
if (rootDirectory) {
|
||||
const rootDirectoryConfig = readLocalConfig(join(path, rootDirectory));
|
||||
|
||||
if (rootDirectoryConfig) {
|
||||
debug(`Read local config from root directory (${rootDirectory})`);
|
||||
localConfig = rootDirectoryConfig;
|
||||
} else if (localConfig) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`The ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} file should be inside of the provided root directory.`,
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
localConfig = localConfig || {};
|
||||
|
||||
if (localConfig.name) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
`The ${code('name')} property in ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} is deprecated (https://vercel.link/name-prop)`,
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
// build `env`
|
||||
const isObject = (item: any) =>
|
||||
Object.prototype.toString.call(item) === '[object Object]';
|
||||
|
||||
// This validation needs to happen on the client side because
|
||||
// the data is merged with other data before it is passed to the API (which
|
||||
// also does schema validation).
|
||||
if (typeof localConfig.env !== 'undefined' && !isObject(localConfig.env)) {
|
||||
error(
|
||||
`The ${code('env')} property in ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} needs to be an object`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (typeof localConfig.build !== 'undefined') {
|
||||
if (!isObject(localConfig.build)) {
|
||||
error(
|
||||
`The ${code('build')} property in ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} needs to be an object`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof localConfig.build.env !== 'undefined' &&
|
||||
!isObject(localConfig.build.env)
|
||||
) {
|
||||
error(
|
||||
`The ${code('build.env')} property in ${highlight(
|
||||
localConfig[fileNameSymbol]!
|
||||
)} needs to be an object`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// build `meta`
|
||||
const meta = Object.assign(
|
||||
{},
|
||||
parseMeta(localConfig.meta),
|
||||
parseMeta(argv['--meta'])
|
||||
);
|
||||
|
||||
// Merge dotenv config, `env` from vercel.json, and `--env` / `-e` arguments
|
||||
const deploymentEnv = Object.assign(
|
||||
{},
|
||||
parseEnv(localConfig.env),
|
||||
parseEnv(argv['--env'])
|
||||
);
|
||||
|
||||
// Merge build env out of `build.env` from vercel.json, and `--build-env` args
|
||||
const deploymentBuildEnv = Object.assign(
|
||||
{},
|
||||
parseEnv(localConfig.build && localConfig.build.env),
|
||||
parseEnv(argv['--build-env'])
|
||||
);
|
||||
|
||||
// If there's any undefined values, then inherit them from this process
|
||||
try {
|
||||
await addProcessEnv(log, deploymentEnv);
|
||||
await addProcessEnv(log, deploymentBuildEnv);
|
||||
} catch (err) {
|
||||
error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// build `regions`
|
||||
const regionFlag = (argv['--regions'] || '')
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean);
|
||||
const regions = regionFlag.length > 0 ? regionFlag : localConfig.regions;
|
||||
|
||||
// build `target`
|
||||
let target;
|
||||
if (argv['--target']) {
|
||||
const deprecatedTarget = argv['--target'];
|
||||
|
||||
if (!['staging', 'production'].includes(deprecatedTarget)) {
|
||||
error(
|
||||
`The specified ${param('--target')} ${code(
|
||||
deprecatedTarget
|
||||
)} is not valid`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deprecatedTarget === 'production') {
|
||||
warn(
|
||||
'We recommend using the much shorter `--prod` option instead of `--target production` (deprecated)'
|
||||
);
|
||||
}
|
||||
|
||||
output.debug(`Setting target to ${deprecatedTarget}`);
|
||||
target = deprecatedTarget;
|
||||
} else if (argv['--prod']) {
|
||||
output.debug('Setting target to production');
|
||||
target = 'production';
|
||||
}
|
||||
|
||||
const currentTeam = org?.type === 'team' ? org.id : undefined;
|
||||
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam });
|
||||
let deployStamp = stamp();
|
||||
let deployment = null;
|
||||
|
||||
try {
|
||||
const createArgs: any = {
|
||||
name: project ? project.name : newProjectName,
|
||||
env: deploymentEnv,
|
||||
build: { env: deploymentBuildEnv },
|
||||
forceNew: argv['--force'],
|
||||
withCache: argv['--with-cache'],
|
||||
quiet,
|
||||
wantsPublic: argv['--public'] || localConfig.public,
|
||||
isFile,
|
||||
type: null,
|
||||
nowConfig: localConfig,
|
||||
regions,
|
||||
meta,
|
||||
deployStamp,
|
||||
target,
|
||||
skipAutoDetectionConfirmation: autoConfirm,
|
||||
};
|
||||
|
||||
if (!localConfig.builds || localConfig.builds.length === 0) {
|
||||
// Only add projectSettings for zero config deployments
|
||||
createArgs.projectSettings = { sourceFilesOutsideRootDirectory };
|
||||
}
|
||||
|
||||
deployment = await createDeploy(
|
||||
client,
|
||||
now,
|
||||
contextName,
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
!project && !isFile,
|
||||
path
|
||||
);
|
||||
|
||||
if (deployment.code === 'missing_project_settings') {
|
||||
let { projectSettings, framework } = deployment;
|
||||
if (rootDirectory) {
|
||||
projectSettings.rootDirectory = rootDirectory;
|
||||
}
|
||||
|
||||
if (typeof sourceFilesOutsideRootDirectory !== 'undefined') {
|
||||
projectSettings.sourceFilesOutsideRootDirectory =
|
||||
sourceFilesOutsideRootDirectory;
|
||||
}
|
||||
|
||||
const settings = await editProjectSettings(
|
||||
output,
|
||||
projectSettings,
|
||||
framework
|
||||
);
|
||||
|
||||
// deploy again, but send projectSettings this time
|
||||
createArgs.projectSettings = settings;
|
||||
|
||||
deployStamp = stamp();
|
||||
createArgs.deployStamp = deployStamp;
|
||||
deployment = await createDeploy(
|
||||
client,
|
||||
now,
|
||||
contextName,
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
false,
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
if (deployment instanceof NotDomainOwner) {
|
||||
output.error(deployment.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deployment instanceof Error) {
|
||||
output.error(
|
||||
deployment.message ||
|
||||
'An unexpected error occurred while deploying your project',
|
||||
undefined,
|
||||
'https://vercel.link/help',
|
||||
'Contact Support'
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deployment.readyState === 'CANCELED') {
|
||||
output.print('The deployment has been canceled.\n');
|
||||
return 1;
|
||||
}
|
||||
|
||||
const deploymentResponse = await getDeploymentByIdOrHost(
|
||||
client,
|
||||
contextName,
|
||||
deployment.id,
|
||||
'v10'
|
||||
);
|
||||
|
||||
if (
|
||||
deploymentResponse instanceof DeploymentNotFound ||
|
||||
deploymentResponse instanceof DeploymentPermissionDenied ||
|
||||
deploymentResponse instanceof InvalidDeploymentId
|
||||
) {
|
||||
output.error(deploymentResponse.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (deployment === null) {
|
||||
error('Uploading failed. Please try again.');
|
||||
return 1;
|
||||
}
|
||||
} catch (err) {
|
||||
debug(`Error: ${err}\n${err.stack}`);
|
||||
|
||||
if (err instanceof NotDomainOwner) {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err instanceof DomainNotFound && err.meta && err.meta.domain) {
|
||||
output.debug(
|
||||
`The domain ${err.meta.domain} was not found, trying to purchase it`
|
||||
);
|
||||
|
||||
const purchase = await purchaseDomainIfAvailable(
|
||||
output,
|
||||
client,
|
||||
err.meta.domain,
|
||||
contextName
|
||||
);
|
||||
|
||||
if (purchase === true) {
|
||||
output.success(`Successfully purchased the domain ${err.meta.domain}!`);
|
||||
|
||||
// We exit if the purchase is completed since
|
||||
// the domain verification can take some time
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (purchase === false || purchase instanceof UserAborted) {
|
||||
handleCreateDeployError(output, deployment, localConfig);
|
||||
return 1;
|
||||
}
|
||||
|
||||
handleCreateDeployError(output, purchase, localConfig);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof DomainNotFound ||
|
||||
err instanceof DomainNotVerified ||
|
||||
err instanceof NotDomainOwner ||
|
||||
err instanceof DomainPermissionDenied ||
|
||||
err instanceof DomainVerificationFailed ||
|
||||
err instanceof SchemaValidationFailed ||
|
||||
err instanceof InvalidDomain ||
|
||||
err instanceof DeploymentNotFound ||
|
||||
err instanceof BuildsRateLimited ||
|
||||
err instanceof DeploymentsRateLimited ||
|
||||
err instanceof AliasDomainConfigured ||
|
||||
err instanceof MissingBuildScript ||
|
||||
err instanceof ConflictingFilePath ||
|
||||
err instanceof ConflictingPathSegment
|
||||
) {
|
||||
handleCreateDeployError(output, err, localConfig);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err instanceof BuildError) {
|
||||
output.error(err.message || 'Build failed');
|
||||
output.error(
|
||||
`Check your logs at https://${now.url}/_logs or run ${getCommandName(
|
||||
`logs ${now.url}`
|
||||
)}`
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (err.keyword === 'additionalProperties' && err.dataPath === '.scale') {
|
||||
const { additionalProperty = '' } = err.params || {};
|
||||
const message = `Invalid DC name for the scale option: ${additionalProperty}`;
|
||||
error(message);
|
||||
}
|
||||
|
||||
if (err.code === 'size_limit_exceeded') {
|
||||
const { sizeLimit = 0 } = err;
|
||||
const message = `File size limit exceeded (${bytes(sizeLimit)})`;
|
||||
error(message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
handleError(err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return printDeploymentStatus(
|
||||
output,
|
||||
client,
|
||||
deployment,
|
||||
deployStamp,
|
||||
!argv['--no-clipboard'],
|
||||
isFile
|
||||
);
|
||||
}
|
||||
|
||||
function handleCreateDeployError(
|
||||
output: Output,
|
||||
error: Error,
|
||||
localConfig: VercelConfig
|
||||
) {
|
||||
if (error instanceof InvalidDomain) {
|
||||
output.error(`The domain ${error.meta.domain} is not valid`);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof DomainVerificationFailed) {
|
||||
output.error(
|
||||
`The domain used as a suffix ${chalk.underline(
|
||||
error.meta.domain
|
||||
)} is not verified and can't be used as custom suffix.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof DomainPermissionDenied) {
|
||||
output.error(
|
||||
`You don't have permissions to access the domain used as a suffix ${chalk.underline(
|
||||
error.meta.domain
|
||||
)}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof SchemaValidationFailed) {
|
||||
const niceError = getPrettyError(error.meta);
|
||||
const fileName = localConfig[fileNameSymbol] || 'vercel.json';
|
||||
niceError.message = `Invalid ${fileName} - ${niceError.message}`;
|
||||
output.prettyError(niceError);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof TooManyRequests) {
|
||||
output.error(
|
||||
`Too many requests detected for ${error.meta.api} API. Try again in ${ms(
|
||||
error.meta.retryAfter * 1000,
|
||||
{
|
||||
long: true,
|
||||
}
|
||||
)}.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof DomainNotVerified) {
|
||||
output.error(
|
||||
`The domain used as an alias ${chalk.underline(
|
||||
error.meta.domain
|
||||
)} is not verified yet. Please verify it.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (error instanceof BuildsRateLimited) {
|
||||
output.error(error.message);
|
||||
output.note(
|
||||
`Run ${getCommandName('upgrade')} to increase your builds limit.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (
|
||||
error instanceof DeploymentNotFound ||
|
||||
error instanceof NotDomainOwner ||
|
||||
error instanceof DeploymentsRateLimited ||
|
||||
error instanceof AliasDomainConfigured ||
|
||||
error instanceof MissingBuildScript ||
|
||||
error instanceof ConflictingFilePath ||
|
||||
error instanceof ConflictingPathSegment
|
||||
) {
|
||||
output.error(error.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -6,15 +6,13 @@ 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';
|
||||
import getSystemEnvValues from '../../util/env/get-system-env-values';
|
||||
|
||||
type Options = {
|
||||
'--debug'?: boolean;
|
||||
'--listen'?: string;
|
||||
'--listen': string;
|
||||
'--confirm': boolean;
|
||||
};
|
||||
|
||||
@@ -27,7 +25,6 @@ export default async function dev(
|
||||
const [dir = '.'] = args;
|
||||
let cwd = resolve(dir);
|
||||
const listen = parseListen(opts['--listen'] || '3000');
|
||||
const debug = opts['--debug'] || false;
|
||||
|
||||
// retrieve dev command
|
||||
let [link, frameworks] = await Promise.all([
|
||||
@@ -36,17 +33,11 @@ export default async function dev(
|
||||
]);
|
||||
|
||||
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
|
||||
const autoConfirm = opts['--confirm'] || false;
|
||||
const forceDelete = false;
|
||||
|
||||
link = await setupAndLink(
|
||||
client,
|
||||
cwd,
|
||||
forceDelete,
|
||||
autoConfirm,
|
||||
'link',
|
||||
'Set up and develop'
|
||||
);
|
||||
link = await setupAndLink(client, cwd, {
|
||||
autoConfirm: opts['--confirm'],
|
||||
successEmoji: 'link',
|
||||
setupMsg: 'Set up and develop',
|
||||
});
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
@@ -79,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +91,6 @@ export default async function dev(
|
||||
|
||||
const devServer = new DevServer(cwd, {
|
||||
output,
|
||||
debug,
|
||||
devCommand,
|
||||
frameworkSlug,
|
||||
projectSettings,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user