mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
90 Commits
@vercel/py
...
@vercel/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d2c0fec89 | ||
|
|
0663524cd7 | ||
|
|
f283b3b106 | ||
|
|
b572ef5d71 | ||
|
|
71e233ce7f | ||
|
|
5669fad6bc | ||
|
|
c294409f5b | ||
|
|
686f78a86e | ||
|
|
c6ed021d2e | ||
|
|
f8cdc943ca | ||
|
|
d8d30b59ec | ||
|
|
a93383cf2e | ||
|
|
49dac4c229 | ||
|
|
1b211f28df | ||
|
|
c2d0887b94 | ||
|
|
fbb8bba4cf | ||
|
|
56cc87fe9d | ||
|
|
0027ffa65b | ||
|
|
0964be1710 | ||
|
|
9618ffe05f | ||
|
|
832ba3fe23 | ||
|
|
253b4fd1d2 | ||
|
|
9e5f17b3c6 | ||
|
|
39d0f8dbfc | ||
|
|
a567d047d1 | ||
|
|
fdfb3a385e | ||
|
|
a630e19896 | ||
|
|
00430eeabf | ||
|
|
49f453742b | ||
|
|
bcb8d4f812 | ||
|
|
d42a8a6588 | ||
|
|
301bcf58fb | ||
|
|
11d0091393 | ||
|
|
6405fb51a1 | ||
|
|
edd477e602 | ||
|
|
4f8f8a5b98 | ||
|
|
a8e66eef41 | ||
|
|
23dd29e269 | ||
|
|
4eb4d2b355 | ||
|
|
3590ea06a4 | ||
|
|
314a97b318 | ||
|
|
d41d9e7374 | ||
|
|
80b211fb4a | ||
|
|
ffaf5c9143 | ||
|
|
ba1c2a7e54 | ||
|
|
30a9183836 | ||
|
|
df9accfd6c | ||
|
|
b388357c0b | ||
|
|
90291525c2 | ||
|
|
812dd43b6a | ||
|
|
9e97e0fd58 | ||
|
|
74528c2160 | ||
|
|
82fd2b8068 | ||
|
|
dd94dcab32 | ||
|
|
300e6c6ebb | ||
|
|
cfe6550ac8 | ||
|
|
dfe009ffe2 | ||
|
|
40f38948a0 | ||
|
|
87eba56063 | ||
|
|
d0a5676c26 | ||
|
|
da9fa997ed | ||
|
|
3d79a9d4d4 | ||
|
|
ae13c5ee92 | ||
|
|
ad654139df | ||
|
|
74f8414e12 | ||
|
|
65c2860e14 | ||
|
|
3b5b397b35 | ||
|
|
051e061176 | ||
|
|
30d46321cc | ||
|
|
c2563535ea | ||
|
|
5f2bed4f24 | ||
|
|
cfb7946f4b | ||
|
|
05c5b3a80d | ||
|
|
45bd855250 | ||
|
|
49de8ad9a0 | ||
|
|
a1ea56fd67 | ||
|
|
e88addc9ed | ||
|
|
5d50013f93 | ||
|
|
44e1eb3983 | ||
|
|
f8af013349 | ||
|
|
972cc495ec | ||
|
|
1c580da3d8 | ||
|
|
244554ab1b | ||
|
|
053c185481 | ||
|
|
8805b586ea | ||
|
|
681070ffa0 | ||
|
|
362b17d60a | ||
|
|
c7c9b1a791 | ||
|
|
c42f309463 | ||
|
|
a0ead28369 |
1
.github/CONTRIBUTING.md
vendored
1
.github/CONTRIBUTING.md
vendored
@@ -13,6 +13,7 @@ To get started, execute the following:
|
||||
```
|
||||
git clone https://github.com/vercel/vercel
|
||||
cd vercel
|
||||
corepack enable
|
||||
yarn install
|
||||
yarn bootstrap
|
||||
yarn build
|
||||
|
||||
17
.github/workflows/cancel.yml
vendored
17
.github/workflows/cancel.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: Cancel
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- '!main'
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
name: 'Cancel Previous Runs'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- uses: styfle/cancel-workflow-action@0.9.1
|
||||
with:
|
||||
workflow_id: test.yml, test-integration-cli.yml, test-unit.yml
|
||||
access_token: ${{ github.token }}
|
||||
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -18,7 +18,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check Release
|
||||
id: check-release
|
||||
run: |
|
||||
|
||||
12
.github/workflows/required-pr-label.yml
vendored
12
.github/workflows/required-pr-label.yml
vendored
@@ -10,13 +10,17 @@ jobs:
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let missing = false;
|
||||
const labels = context.payload.pull_request.labels.map(l => l.name);
|
||||
if (labels.filter(l => l.startsWith('area:')).length === 0) {
|
||||
console.error('\u001b[31mMissing label: Please add at least one "area" label.');
|
||||
process.exit(1);
|
||||
console.error('::error::Missing label: Please add at least one "area" label.');
|
||||
missing = true;
|
||||
}
|
||||
if (labels.filter(l => l.startsWith('semver:')).length !== 1) {
|
||||
console.error('\u001b[31mMissing label: Please add exactly one "semver" label.');
|
||||
console.error('::error::Missing label: Please add exactly one "semver" label.');
|
||||
missing = true;
|
||||
}
|
||||
if (missing) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\u001b[32mSuccess: This pull request has correct labels, thanks!');
|
||||
console.log('::notice::Success: This pull request has correct labels, thanks!');
|
||||
|
||||
4
.github/workflows/test-integration-cli.yml
vendored
4
.github/workflows/test-integration-cli.yml
vendored
@@ -13,6 +13,10 @@ env:
|
||||
TURBO_TEAM: 'vercel'
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: CLI
|
||||
|
||||
4
.github/workflows/test-unit.yml
vendored
4
.github/workflows/test-unit.yml
vendored
@@ -13,6 +13,10 @@ env:
|
||||
TURBO_TEAM: 'vercel'
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Unit
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -14,6 +14,10 @@ env:
|
||||
TURBO_TEAM: 'vercel'
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Find Changes
|
||||
@@ -78,11 +82,11 @@ jobs:
|
||||
- run: yarn install --network-timeout 1000000
|
||||
|
||||
- name: Build ${{matrix.packageName}} and all its dependencies
|
||||
run: node_modules/.bin/turbo run build --cache-dir=".turbo" --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run build --cache-dir=".turbo" --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
env:
|
||||
FORCE_COLOR: '1'
|
||||
- name: Test ${{matrix.packageName}}
|
||||
run: node_modules/.bin/turbo run test --cache-dir=".turbo" --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run test --cache-dir=".turbo" --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
|
||||
shell: bash
|
||||
env:
|
||||
VERCEL_CLI_VERSION: ${{ needs.setup.outputs.dplUrl }}/tarballs/vercel.tgz
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ test/lib/deployment/failed-page.txt
|
||||
__pycache__
|
||||
.vercel
|
||||
.turbo
|
||||
turbo-cache-key.json
|
||||
|
||||
@@ -380,8 +380,8 @@ This is a [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refere
|
||||
|
||||
This is an abstract enumeration type that is implemented by one of the following possible `String` values:
|
||||
|
||||
- `nodejs16.x`
|
||||
- `nodejs14.x`
|
||||
- `nodejs12.x`
|
||||
- `go1.x`
|
||||
- `java11`
|
||||
- `python3.9`
|
||||
|
||||
@@ -19,11 +19,9 @@
|
||||
|
||||
## Vercel
|
||||
|
||||
Vercel is a platform for **static sites and frontend frameworks**, built to integrate with your headless content, commerce, or database.
|
||||
Vercel is the platform for frontend developers, providing the speed and reliability innovators need to create at the moment of inspiration.
|
||||
|
||||
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.
|
||||
We enable teams to iterate quickly and develop, preview, and ship delightful user experiences. Vercel has zero-configuration support for 35+ frontend frameworks and integrates with your headless content, commerce, or database of choice.
|
||||
|
||||
## Deploy
|
||||
|
||||
@@ -37,4 +35,4 @@ For details on how to use Vercel, check out our [documentation](https://vercel.c
|
||||
|
||||
- [Code of Conduct](./.github/CODE_OF_CONDUCT.md)
|
||||
- [Contributing Guidelines](./.github/CONTRIBUTING.md)
|
||||
- [MIT License](./LICENSE)
|
||||
- [Apache 2.0 License](./LICENSE)
|
||||
|
||||
@@ -18,6 +18,12 @@ async function main() {
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
|
||||
await fs.cp(
|
||||
join(repoRoot, 'packages', 'fs-detectors', 'logos'),
|
||||
join(pubDir, 'monorepo-logos'),
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
|
||||
const examples = await getExampleList();
|
||||
const pathListAll = join(pubDir, 'list-all.json');
|
||||
await fs.writeFile(pathListAll, JSON.stringify(examples));
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"unzip-stream": "0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "13.1.4",
|
||||
"@types/node": "14.18.33",
|
||||
"@types/node-fetch": "2.5.4",
|
||||
"@vercel/node": "1.9.0",
|
||||
"typescript": "3.9.6"
|
||||
|
||||
13
errors/deployment-error.md
Normal file
13
errors/deployment-error.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Deployment failed
|
||||
|
||||
## Why This Error Occurred
|
||||
|
||||
When deploying this project, there was not a successful deployment into the `READY` state.
|
||||
|
||||
## How to Fix It
|
||||
|
||||
This is a generic error to catch problems in the deployment. The error is likely to vary depending on the deployment and the conditions at the time.
|
||||
|
||||
Try looking in the logs for information about the deployment and the failure, this could be done at (vercel.com)[https://vercel.com]. You can also use the `vc logs` command to display the build logs for the deployment.
|
||||
|
||||
This might not be a permanent error and retrying the deployment might also resolve it.
|
||||
@@ -8,6 +8,7 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^1.0.0-rc.8"
|
||||
"astro": "^1.0.0-rc.8",
|
||||
"web-vitals": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
8
examples/astro/src/env.d.ts
vendored
8
examples/astro/src/env.d.ts
vendored
@@ -1 +1,9 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly PUBLIC_VERCEL_ANALYTICS_ID: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,19 @@ const { title } = Astro.props as Props;
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<script>
|
||||
import { webVitals } from "../lib/vitals";
|
||||
|
||||
let analyticsId = import.meta.env.PUBLIC_VERCEL_ANALYTICS_ID;
|
||||
|
||||
if (analyticsId) {
|
||||
webVitals({
|
||||
path: location.pathname,
|
||||
params: location.search,
|
||||
analyticsId,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
|
||||
66
examples/astro/src/lib/vitals.js
Normal file
66
examples/astro/src/lib/vitals.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { onCLS, onFCP, onFID, onLCP, onTTFB } from "web-vitals";
|
||||
|
||||
const vitalsUrl = "https://vitals.vercel-analytics.com/v1/vitals";
|
||||
|
||||
function getConnectionSpeed() {
|
||||
return "connection" in navigator &&
|
||||
navigator["connection"] &&
|
||||
"effectiveType" in navigator["connection"]
|
||||
? // @ts-ignore
|
||||
navigator["connection"]["effectiveType"]
|
||||
: "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("web-vitals").Metric} metric
|
||||
* @param {{ params: { [s: string]: any; } | ArrayLike<any>; path: string; analyticsId: string; debug: boolean; }} options
|
||||
*/
|
||||
export function sendToAnalytics(metric, options) {
|
||||
const page = Object.entries(options.params).reduce(
|
||||
(acc, [key, value]) => acc.replace(value, `[${key}]`),
|
||||
options.path
|
||||
);
|
||||
|
||||
const body = {
|
||||
dsn: options.analyticsId,
|
||||
id: metric.id,
|
||||
page,
|
||||
href: location.href,
|
||||
event_name: metric.name,
|
||||
value: metric.value.toString(),
|
||||
speed: getConnectionSpeed(),
|
||||
};
|
||||
|
||||
if (options.debug) {
|
||||
console.log("[Analytics]", metric.name, JSON.stringify(body, null, 2));
|
||||
}
|
||||
|
||||
const blob = new Blob([new URLSearchParams(body).toString()], {
|
||||
// This content type is necessary for `sendBeacon`
|
||||
type: "application/x-www-form-urlencoded",
|
||||
});
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(vitalsUrl, blob);
|
||||
} else
|
||||
fetch(vitalsUrl, {
|
||||
body: blob,
|
||||
method: "POST",
|
||||
credentials: "omit",
|
||||
keepalive: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} options
|
||||
*/
|
||||
export function webVitals(options) {
|
||||
try {
|
||||
onFID((metric) => sendToAnalytics(metric, options));
|
||||
onTTFB((metric) => sendToAnalytics(metric, options));
|
||||
onLCP((metric) => sendToAnalytics(metric, options));
|
||||
onCLS((metric) => sendToAnalytics(metric, options));
|
||||
onFCP((metric) => sendToAnalytics(metric, options));
|
||||
} catch (err) {
|
||||
console.error("[Analytics]", err);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
|
||||
4745
examples/nextjs/package-lock.json
generated
Normal file
4745
examples/nextjs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,12 +9,10 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "12.3.1",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.3",
|
||||
"next": "13.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.23.1",
|
||||
"eslint-config-next": "12.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export default function Home() {
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.card}
|
||||
>
|
||||
<h2>Deploy →</h2>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,13 @@ This directory is a brief example of a [Remix](https://remix.run/docs) site that
|
||||
|
||||
_Live Example: https://remix-run-template.vercel.app_
|
||||
|
||||
You can also deploy using the [Vercel CLI](https://vercel.com/cli):
|
||||
|
||||
```sh
|
||||
npm i -g vercel
|
||||
vercel
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To run your Remix app locally, make sure your project's local dependencies are installed:
|
||||
@@ -23,5 +30,3 @@ npm run dev
|
||||
```
|
||||
|
||||
Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go!
|
||||
|
||||
If you're used to using the `vercel dev` command provided by [Vercel CLI](https://vercel.com/cli) instead, you can also use that, but it's not needed.
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
import { RemixBrowser } from "@remix-run/react";
|
||||
import { hydrate } from "react-dom";
|
||||
import { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
|
||||
hydrate(<RemixBrowser />, document);
|
||||
function hydrate() {
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<RemixBrowser />
|
||||
</StrictMode>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(hydrate);
|
||||
} else {
|
||||
// Safari doesn't support requestIdleCallback
|
||||
// https://caniuse.com/requestidlecallback
|
||||
window.setTimeout(hydrate, 1);
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ export default function handleRequest(
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
let markup = renderToString(
|
||||
const markup = renderToString(
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
return new Response("<!DOCTYPE html>" + markup, {
|
||||
status: responseStatusCode,
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,182 +1,34 @@
|
||||
import type { LinksFunction, MetaFunction } from "@remix-run/node";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import {
|
||||
Link,
|
||||
Links,
|
||||
LiveReload,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useCatch,
|
||||
} from "@remix-run/react";
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
|
||||
import darkStylesUrl from "~/styles/dark.css";
|
||||
import globalStylesUrl from "~/styles/global.css";
|
||||
|
||||
// https://remix.run/api/conventions#links
|
||||
export let links: LinksFunction = () => {
|
||||
return [
|
||||
{ rel: "stylesheet", href: globalStylesUrl },
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: darkStylesUrl,
|
||||
media: "(prefers-color-scheme: dark)"
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// https://remix.run/api/conventions#meta
|
||||
export let meta: MetaFunction = () => ({
|
||||
export const meta: MetaFunction = () => ({
|
||||
charset: "utf-8",
|
||||
title: "New Remix App",
|
||||
viewport: "width=device-width,initial-scale=1",
|
||||
});
|
||||
|
||||
// https://remix.run/api/conventions#default-export
|
||||
// https://remix.run/api/conventions#route-filenames
|
||||
export default function App() {
|
||||
return (
|
||||
<Document>
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
// https://remix.run/api/conventions#errorboundary
|
||||
export function ErrorBoundary({ error }: { error: Error }) {
|
||||
console.error(error);
|
||||
return (
|
||||
<Document title="Error!">
|
||||
<Layout>
|
||||
<div>
|
||||
<h1>There was an error</h1>
|
||||
<p>{error.message}</p>
|
||||
<hr />
|
||||
<p>
|
||||
Hey, developer, you should replace this with what you want your
|
||||
users to see.
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
// https://remix.run/api/conventions#catchboundary
|
||||
export function CatchBoundary() {
|
||||
let caught = useCatch();
|
||||
|
||||
let message;
|
||||
switch (caught.status) {
|
||||
case 401:
|
||||
message = (
|
||||
<p>
|
||||
Oops! Looks like you tried to visit a page that you do not have access
|
||||
to.
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case 404:
|
||||
message = (
|
||||
<p>Oops! Looks like you tried to visit a page that does not exist.</p>
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(caught.data || caught.statusText);
|
||||
}
|
||||
|
||||
return (
|
||||
<Document title={`${caught.status} ${caught.statusText}`}>
|
||||
<Layout>
|
||||
<h1>
|
||||
{caught.status}: {caught.statusText}
|
||||
</h1>
|
||||
{message}
|
||||
</Layout>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
function Document({
|
||||
children,
|
||||
title
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
{title ? <title>{title}</title> : null}
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="remix-app">
|
||||
<header className="remix-app__header">
|
||||
<div className="container remix-app__header-content">
|
||||
<Link to="/" title="Remix" className="remix-app__header-home-link">
|
||||
<RemixLogo />
|
||||
</Link>
|
||||
<nav aria-label="Main navigation" className="remix-app__header-nav">
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/">Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://remix.run/docs">Remix Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/remix-run/remix">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div className="remix-app__main">
|
||||
<div className="container remix-app__main-content">{children}</div>
|
||||
</div>
|
||||
<footer className="remix-app__footer">
|
||||
<div className="container remix-app__footer-content">
|
||||
<p>© You!</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RemixLogo() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 659 165"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
aria-labelledby="remix-run-logo-title"
|
||||
role="img"
|
||||
width="106"
|
||||
height="30"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title id="remix-run-logo-title">Remix Logo</title>
|
||||
<path d="M0 161V136H45.5416C53.1486 136 54.8003 141.638 54.8003 145V161H0Z M133.85 124.16C135.3 142.762 135.3 151.482 135.3 161H92.2283C92.2283 158.927 92.2653 157.03 92.3028 155.107C92.4195 149.128 92.5411 142.894 91.5717 130.304C90.2905 111.872 82.3473 107.776 67.7419 107.776H54.8021H0V74.24H69.7918C88.2407 74.24 97.4651 68.632 97.4651 53.784C97.4651 40.728 88.2407 32.816 69.7918 32.816H0V0H77.4788C119.245 0 140 19.712 140 51.2C140 74.752 125.395 90.112 105.665 92.672C122.32 96 132.057 105.472 133.85 124.16Z" />
|
||||
<path d="M229.43 120.576C225.59 129.536 218.422 133.376 207.158 133.376C194.614 133.376 184.374 126.72 183.35 112.64H263.478V101.12C263.478 70.1437 243.254 44.0317 205.11 44.0317C169.526 44.0317 142.902 69.8877 142.902 105.984C142.902 142.336 169.014 164.352 205.622 164.352C235.83 164.352 256.822 149.76 262.71 123.648L229.43 120.576ZM183.862 92.6717C185.398 81.9197 191.286 73.7277 204.598 73.7277C216.886 73.7277 223.542 82.4317 224.054 92.6717H183.862Z" />
|
||||
<path d="M385.256 66.5597C380.392 53.2477 369.896 44.0317 349.672 44.0317C332.52 44.0317 320.232 51.7117 314.088 64.2557V47.1037H272.616V161.28H314.088V105.216C314.088 88.0638 318.952 76.7997 332.52 76.7997C345.064 76.7997 348.136 84.9917 348.136 100.608V161.28H389.608V105.216C389.608 88.0638 394.216 76.7997 408.04 76.7997C420.584 76.7997 423.4 84.9917 423.4 100.608V161.28H464.872V89.5997C464.872 65.7917 455.656 44.0317 424.168 44.0317C404.968 44.0317 391.4 53.7597 385.256 66.5597Z" />
|
||||
<path d="M478.436 47.104V161.28H519.908V47.104H478.436ZM478.18 36.352H520.164V0H478.18V36.352Z" />
|
||||
<path d="M654.54 47.1035H611.788L592.332 74.2395L573.388 47.1035H527.564L568.78 103.168L523.98 161.28H566.732L589.516 130.304L612.3 161.28H658.124L613.068 101.376L654.54 47.1035Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { MetaFunction, LinksFunction } from "@remix-run/node";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
|
||||
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/docs/en/v1/guides/routing">
|
||||
read all about nested routes in the docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<hr />
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { ActionFunction } from "@remix-run/node";
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function NiceWork() {
|
||||
return <h1>You got it right!</h1>;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Link, Outlet, useCatch, useLoaderData } from "@remix-run/react";
|
||||
|
||||
export let meta: MetaFunction = () => ({ 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>
|
||||
);
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import type { LoaderFunction, MetaFunction } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Link, useCatch, useLoaderData } from "@remix-run/react";
|
||||
|
||||
// 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...",
|
||||
};
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { LoaderFunction } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Link, Outlet, useCatch, useLoaderData } from "@remix-run/react";
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +1,32 @@
|
||||
import type { MetaFunction, LoaderFunction } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
|
||||
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 style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
|
||||
<h1>Welcome to Remix</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://remix.run/tutorials/blog"
|
||||
rel="noreferrer"
|
||||
>
|
||||
15m Quickstart Blog Tutorial
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://remix.run/tutorials/jokes"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Deep Dive Jokes App Tutorial
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
|
||||
Remix Docs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
: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%);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
/*
|
||||
* You can just delete everything here or keep whatever you like, it's just a
|
||||
* quick baseline!
|
||||
*/
|
||||
:root {
|
||||
--color-foreground: hsl(0, 0%, 7%);
|
||||
--color-background: hsl(0, 0%, 100%);
|
||||
--color-links: hsl(213, 100%, 52%);
|
||||
--color-links-hover: hsl(213, 100%, 43%);
|
||||
--color-border: hsl(0, 0%, 82%);
|
||||
--font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica, Roboto,
|
||||
Arial, sans-serif, system-ui, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: var(--color-links) solid 2px;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-links);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-links-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background-color: var(--color-border);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
input:where([type="text"]),
|
||||
input:where([type="search"]) {
|
||||
display: block;
|
||||
border: 1px solid var(--color-border);
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
height: calc(1ch + 1.5em);
|
||||
padding-right: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
background-color: hsl(0 0% 100% / 20%);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
--gutter: 16px;
|
||||
width: 1024px;
|
||||
max-width: calc(100% - var(--gutter) * 2);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.remix-app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: calc(100vh - env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.remix-app > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remix-app__header {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.remix-app__header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remix-app__header-home-link {
|
||||
width: 106px;
|
||||
height: 30px;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.remix-app__header-nav ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
.remix-app__header-nav li {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remix-app__main {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.remix-app__footer {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.remix-app__footer-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remix__page {
|
||||
--gap: 1rem;
|
||||
--space: 2rem;
|
||||
display: grid;
|
||||
grid-auto-rows: min-content;
|
||||
gap: var(--gap);
|
||||
padding-top: var(--space);
|
||||
padding-bottom: var(--space);
|
||||
}
|
||||
|
||||
@media print, screen and (min-width: 640px) {
|
||||
.remix__page {
|
||||
--gap: 2rem;
|
||||
grid-auto-rows: unset;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.remix__page {
|
||||
--gap: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.remix__page > main > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.remix__page > main > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.remix__page > aside {
|
||||
margin: 0;
|
||||
padding: 1.5ch 2ch;
|
||||
border: solid 1px var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.remix__page > aside > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.remix__page > aside > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.remix__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.remix__form > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
23926
examples/remix/package-lock.json
generated
Normal file
23926
examples/remix/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,21 +6,22 @@
|
||||
"dev": "remix dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/node": "^1.5.1",
|
||||
"@remix-run/react": "^1.5.1",
|
||||
"@remix-run/vercel": "^1.5.1",
|
||||
"@vercel/node": "^2.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"@remix-run/node": "^1.7.5",
|
||||
"@remix-run/react": "^1.7.5",
|
||||
"@remix-run/vercel": "^1.7.5",
|
||||
"@vercel/analytics": "^0.1.3",
|
||||
"@vercel/node": "^2.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.5.1",
|
||||
"@remix-run/eslint-config": "^1.5.1",
|
||||
"@remix-run/serve": "^1.5.1",
|
||||
"@types/react": "^17.0.45",
|
||||
"@types/react-dom": "^17.0.17",
|
||||
"eslint": "^8.15.0",
|
||||
"typescript": "^4.6.4"
|
||||
"@remix-run/dev": "^1.7.5",
|
||||
"@remix-run/eslint-config": "^1.7.5",
|
||||
"@remix-run/serve": "^1.7.5",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"eslint": "^8.23.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* @type {import('@remix-run/dev').AppConfig}
|
||||
*/
|
||||
/** @type {import('@remix-run/dev').AppConfig} */
|
||||
module.exports = {
|
||||
serverBuildTarget: "vercel",
|
||||
// When running locally in development mode, we use the built in remix
|
||||
|
||||
2
examples/remix/remix.env.d.ts
vendored
2
examples/remix/remix.env.d.ts
vendored
@@ -1,2 +1,2 @@
|
||||
/// <reference types="@remix-run/dev" />
|
||||
/// <reference types="@remix-run/node/globals" />
|
||||
/// <reference types="@remix-run/node" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as build from "@remix-run/dev/server-build";
|
||||
import { createRequestHandler } from "@remix-run/vercel";
|
||||
import * as build from "@remix-run/dev/server-build";
|
||||
|
||||
export default createRequestHandler({ build, mode: process.env.NODE_ENV });
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2019",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"solid-app-router": "^0.3.2",
|
||||
"solid-js": "^1.3.15",
|
||||
"solid-meta": "^0.27.3",
|
||||
"solid-start": "next",
|
||||
"solid-start-vercel": "next",
|
||||
"vite": "^2.9.9"
|
||||
"solid-start-vercel": "^0.2.0",
|
||||
"typescript": "^4.8.3",
|
||||
"vite": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/meta": "^0.28.2",
|
||||
"@solidjs/router": "^0.5.0",
|
||||
"solid-js": "^1.6.0",
|
||||
"solid-start": "^0.2.0",
|
||||
"undici": "^5.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16.x"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { hydrate } from "solid-js/web";
|
||||
import { StartClient } from "solid-start/entry-client";
|
||||
import { mount, StartClient } from 'solid-start/entry-client';
|
||||
|
||||
hydrate(() => <StartClient />, document);
|
||||
mount(() => <StartClient />, document);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { StartServer, createHandler, renderAsync } from "solid-start/entry-server";
|
||||
import { inlineServerModules } from "solid-start/server";
|
||||
import {
|
||||
StartServer,
|
||||
createHandler,
|
||||
renderAsync,
|
||||
} from 'solid-start/entry-server';
|
||||
|
||||
export default createHandler(
|
||||
inlineServerModules,
|
||||
renderAsync((context) => <StartServer context={context} />)
|
||||
renderAsync(event => <StartServer event={event} />)
|
||||
);
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
// @refresh reload
|
||||
import { Links, Meta, Routes, Scripts } from "solid-start/root";
|
||||
import { ErrorBoundary } from "solid-start/error-boundary";
|
||||
import { Suspense } from "solid-js";
|
||||
import {
|
||||
Html,
|
||||
Head,
|
||||
Body,
|
||||
Meta,
|
||||
Routes,
|
||||
FileRoutes,
|
||||
Scripts,
|
||||
ErrorBoundary,
|
||||
} from 'solid-start';
|
||||
import { Suspense } from 'solid-js';
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<Meta charset="utf-8" />
|
||||
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<Body>
|
||||
<ErrorBoundary>
|
||||
<Suspense>
|
||||
<Routes />
|
||||
<Routes>
|
||||
<FileRoutes />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export default function Home() {
|
||||
<Counter />
|
||||
<p>
|
||||
Visit{" "}
|
||||
<a href="https://solidjs.com" target="_blank">
|
||||
solidjs.com
|
||||
<a href="https://start.solidjs.com" target="_blank">
|
||||
start.solidjs.com
|
||||
</a>{" "}
|
||||
to learn how to build Solid apps.
|
||||
</p>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"env": {
|
||||
"ENABLE_VC_BUILD": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from "vite";
|
||||
import solid from "solid-start";
|
||||
import solid from "solid-start/vite";
|
||||
import vercel from "solid-start-vercel";
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -3,6 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
@@ -43,14 +44,14 @@
|
||||
"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": "turbo run build",
|
||||
"build": "node utils/gen.js && turbo run build",
|
||||
"vercel-build": "yarn build && yarn run pack && cd api && node -r ts-eager/register ./_lib/script/build.ts",
|
||||
"pre-commit": "lint-staged",
|
||||
"test": "jest --rootDir=\"test\" --testPathPattern=\"\\.test.js\"",
|
||||
"test-unit": "yarn test && turbo run test-unit",
|
||||
"test-integration-cli": "turbo run test-integration-cli",
|
||||
"test-integration-once": "turbo run test-integration-once",
|
||||
"test-integration-dev": "turbo run test-integration-dev",
|
||||
"test-unit": "yarn test && node utils/gen.js && turbo run test-unit",
|
||||
"test-integration-cli": "node utils/gen.js && turbo run test-integration-cli",
|
||||
"test-integration-once": "node utils/gen.js && turbo run test-integration-once",
|
||||
"test-integration-dev": "node utils/gen.js && turbo run test-integration-dev",
|
||||
"lint": "eslint . --ext .ts,.js",
|
||||
"prepare": "husky install",
|
||||
"pack": "cd utils && node -r ts-eager/register ./pack.ts"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "5.5.3",
|
||||
"version": "5.5.7",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
@@ -39,7 +39,7 @@ export class EdgeFunction {
|
||||
assets?: { name: string; path: string }[];
|
||||
|
||||
/** The regions where the edge function will be executed on */
|
||||
regions?: 'auto' | string[] | 'all' | 'default';
|
||||
regions?: string | string[];
|
||||
|
||||
constructor(params: Omit<EdgeFunction, 'type'>) {
|
||||
this.type = 'EdgeFunction';
|
||||
|
||||
@@ -5,7 +5,13 @@ import path from 'path';
|
||||
import Sema from 'async-sema';
|
||||
import { FileBase } from './types';
|
||||
|
||||
const semaToPreventEMFILE = new Sema(20);
|
||||
const DEFAULT_SEMA = 20;
|
||||
const semaToPreventEMFILE = new Sema(
|
||||
parseInt(
|
||||
process.env.VERCEL_INTERNAL_FILE_FS_REF_SEMA || String(DEFAULT_SEMA),
|
||||
10
|
||||
) || DEFAULT_SEMA
|
||||
);
|
||||
|
||||
interface FileFsRefOptions {
|
||||
mode?: number;
|
||||
|
||||
@@ -12,7 +12,13 @@ interface FileRefOptions {
|
||||
mutable?: boolean;
|
||||
}
|
||||
|
||||
const semaToDownloadFromS3 = new Sema(5);
|
||||
const DEFAULT_SEMA = 5;
|
||||
const semaToDownloadFromS3 = new Sema(
|
||||
parseInt(
|
||||
process.env.VERCEL_INTERNAL_FILE_REF_SEMA || String(DEFAULT_SEMA),
|
||||
10
|
||||
) || DEFAULT_SEMA
|
||||
);
|
||||
|
||||
class BailableError extends Error {
|
||||
public bail: boolean;
|
||||
|
||||
@@ -3,28 +3,37 @@ import { NodeVersion } from '../types';
|
||||
import { NowBuildError } from '../errors';
|
||||
import debug from '../debug';
|
||||
|
||||
const allOptions = [
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
{ major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||
{
|
||||
major: 12,
|
||||
range: '12.x',
|
||||
runtime: 'nodejs12.x',
|
||||
discontinueDate: new Date('2022-10-03'),
|
||||
},
|
||||
{
|
||||
major: 10,
|
||||
range: '10.x',
|
||||
runtime: 'nodejs10.x',
|
||||
discontinueDate: new Date('2021-04-20'),
|
||||
},
|
||||
{
|
||||
major: 8,
|
||||
range: '8.10.x',
|
||||
runtime: 'nodejs8.10',
|
||||
discontinueDate: new Date('2020-01-06'),
|
||||
},
|
||||
] as const;
|
||||
function getOptions() {
|
||||
const options = [
|
||||
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||
{ major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||
{
|
||||
major: 12,
|
||||
range: '12.x',
|
||||
runtime: 'nodejs12.x',
|
||||
discontinueDate: new Date('2022-10-03'),
|
||||
},
|
||||
{
|
||||
major: 10,
|
||||
range: '10.x',
|
||||
runtime: 'nodejs10.x',
|
||||
discontinueDate: new Date('2021-04-20'),
|
||||
},
|
||||
{
|
||||
major: 8,
|
||||
range: '8.10.x',
|
||||
runtime: 'nodejs8.10',
|
||||
discontinueDate: new Date('2020-01-06'),
|
||||
},
|
||||
] as const;
|
||||
if (process.env.VERCEL_ALLOW_NODEJS18 === '1') {
|
||||
return [
|
||||
{ major: 18, range: '18.x', runtime: 'nodejs18.x' },
|
||||
...options,
|
||||
] as const;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function getHint(isAuto = false) {
|
||||
const { major, range } = getLatestNodeVersion();
|
||||
@@ -34,11 +43,11 @@ function getHint(isAuto = false) {
|
||||
}
|
||||
|
||||
export function getLatestNodeVersion() {
|
||||
return allOptions[0];
|
||||
return getOptions()[0];
|
||||
}
|
||||
|
||||
export function getDiscontinuedNodeVersions(): NodeVersion[] {
|
||||
return allOptions.filter(isDiscontinued);
|
||||
return getOptions().filter(isDiscontinued);
|
||||
}
|
||||
|
||||
export async function getSupportedNodeVersion(
|
||||
@@ -50,7 +59,7 @@ export async function getSupportedNodeVersion(
|
||||
if (engineRange) {
|
||||
const found =
|
||||
validRange(engineRange) &&
|
||||
allOptions.some(o => {
|
||||
getOptions().some(o => {
|
||||
// the array is already in order so return the first
|
||||
// match which will be the newest version of node
|
||||
selection = o;
|
||||
|
||||
@@ -251,33 +251,24 @@ export async function getNodeVersion(
|
||||
meta: Meta = {}
|
||||
): Promise<NodeVersion> {
|
||||
const latest = getLatestNodeVersion();
|
||||
if (meta && meta.isDev) {
|
||||
if (meta.isDev) {
|
||||
// Use the system-installed version of `node` in PATH for `vercel dev`
|
||||
return { ...latest, runtime: 'nodejs' };
|
||||
}
|
||||
const { packageJson } = await scanParentDirs(destPath, true);
|
||||
let { nodeVersion } = config;
|
||||
let isAuto = true;
|
||||
if (packageJson && packageJson.engines && packageJson.engines.node) {
|
||||
if (packageJson?.engines?.node) {
|
||||
const { node } = packageJson.engines;
|
||||
if (
|
||||
nodeVersion &&
|
||||
validRange(node) &&
|
||||
!intersects(nodeVersion, node) &&
|
||||
!meta.isDev
|
||||
) {
|
||||
if (nodeVersion && validRange(node) && !intersects(nodeVersion, node)) {
|
||||
console.warn(
|
||||
`Warning: Due to "engines": { "node": "${node}" } in your \`package.json\` file, the Node.js Version defined in your Project Settings ("${nodeVersion}") will not apply. Learn More: http://vercel.link/node-version`
|
||||
);
|
||||
} else if (coerce(node)?.raw === node && !meta.isDev) {
|
||||
} else if (coerce(node)?.raw === node) {
|
||||
console.warn(
|
||||
`Warning: Detected "engines": { "node": "${node}" } in your \`package.json\` with major.minor.patch, but only major Node.js Version can be selected. Learn More: http://vercel.link/node-version`
|
||||
);
|
||||
} else if (
|
||||
validRange(node) &&
|
||||
intersects(`${latest.major + 1}.x`, node) &&
|
||||
!meta.isDev
|
||||
) {
|
||||
} else if (validRange(node) && intersects(`${latest.major + 1}.x`, node)) {
|
||||
console.warn(
|
||||
`Warning: Detected "engines": { "node": "${node}" } in your \`package.json\` that will automatically upgrade when a new major Node.js Version is released. Learn More: http://vercel.link/node-version`
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface LambdaOptionsBase {
|
||||
regions?: string[];
|
||||
supportsMultiPayloads?: boolean;
|
||||
supportsWrapper?: boolean;
|
||||
experimentalResponseStreaming?: boolean;
|
||||
}
|
||||
|
||||
export interface LambdaOptionsWithFiles extends LambdaOptionsBase {
|
||||
@@ -60,6 +61,7 @@ export class Lambda {
|
||||
zipBuffer?: Buffer;
|
||||
supportsMultiPayloads?: boolean;
|
||||
supportsWrapper?: boolean;
|
||||
experimentalResponseStreaming?: boolean;
|
||||
|
||||
constructor(opts: LambdaOptions) {
|
||||
const {
|
||||
@@ -72,6 +74,7 @@ export class Lambda {
|
||||
regions,
|
||||
supportsMultiPayloads,
|
||||
supportsWrapper,
|
||||
experimentalResponseStreaming,
|
||||
} = opts;
|
||||
if ('files' in opts) {
|
||||
assert(typeof opts.files === 'object', '"files" must be an object');
|
||||
@@ -132,6 +135,7 @@ export class Lambda {
|
||||
this.zipBuffer = 'zipBuffer' in opts ? opts.zipBuffer : undefined;
|
||||
this.supportsMultiPayloads = supportsMultiPayloads;
|
||||
this.supportsWrapper = supportsWrapper;
|
||||
this.experimentalResponseStreaming = experimentalResponseStreaming;
|
||||
}
|
||||
|
||||
async createZip(): Promise<Buffer> {
|
||||
|
||||
@@ -8,6 +8,8 @@ interface PrerenderOptions {
|
||||
group?: number;
|
||||
bypassToken?: string | null /* optional to be non-breaking change */;
|
||||
allowQuery?: string[];
|
||||
initialHeaders?: Record<string, string>;
|
||||
initialStatus?: number;
|
||||
}
|
||||
|
||||
export class Prerender {
|
||||
@@ -18,6 +20,8 @@ export class Prerender {
|
||||
public group?: number;
|
||||
public bypassToken: string | null;
|
||||
public allowQuery?: string[];
|
||||
public initialHeaders?: Record<string, string>;
|
||||
public initialStatus?: number;
|
||||
|
||||
constructor({
|
||||
expiration,
|
||||
@@ -26,6 +30,8 @@ export class Prerender {
|
||||
group,
|
||||
bypassToken,
|
||||
allowQuery,
|
||||
initialHeaders,
|
||||
initialStatus,
|
||||
}: PrerenderOptions) {
|
||||
this.type = 'Prerender';
|
||||
this.expiration = expiration;
|
||||
@@ -64,6 +70,30 @@ export class Prerender {
|
||||
}
|
||||
this.fallback = fallback;
|
||||
|
||||
if (initialHeaders !== undefined) {
|
||||
if (
|
||||
!initialHeaders ||
|
||||
typeof initialHeaders !== 'object' ||
|
||||
Object.entries(initialHeaders).some(
|
||||
([key, value]) => typeof key !== 'string' || typeof value !== 'string'
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`The \`initialHeaders\` argument for \`Prerender\` must be an object with string key/values`
|
||||
);
|
||||
}
|
||||
this.initialHeaders = initialHeaders;
|
||||
}
|
||||
|
||||
if (initialStatus !== undefined) {
|
||||
if (initialStatus <= 0 || !Number.isInteger(initialStatus)) {
|
||||
throw new Error(
|
||||
`The \`initialStatus\` argument for \`Prerender\` must be a natural number.`
|
||||
);
|
||||
}
|
||||
this.initialStatus = initialStatus;
|
||||
}
|
||||
|
||||
if (allowQuery !== undefined) {
|
||||
if (!Array.isArray(allowQuery)) {
|
||||
throw new Error(
|
||||
|
||||
79
packages/build-utils/test/unit.test.ts
vendored
79
packages/build-utils/test/unit.test.ts
vendored
@@ -216,10 +216,6 @@ it('should download symlinks even with incorrect file', async () => {
|
||||
});
|
||||
|
||||
it('should only match supported node versions, otherwise throw an error', async () => {
|
||||
expect(await getSupportedNodeVersion('12.x', false)).toHaveProperty(
|
||||
'major',
|
||||
12
|
||||
);
|
||||
expect(await getSupportedNodeVersion('14.x', false)).toHaveProperty(
|
||||
'major',
|
||||
14
|
||||
@@ -240,10 +236,6 @@ it('should only match supported node versions, otherwise throw an error', async
|
||||
await expectBuilderError(getSupportedNodeVersion('foo', true), autoMessage);
|
||||
await expectBuilderError(getSupportedNodeVersion('=> 10', true), autoMessage);
|
||||
|
||||
expect(await getSupportedNodeVersion('12.x', true)).toHaveProperty(
|
||||
'major',
|
||||
12
|
||||
);
|
||||
expect(await getSupportedNodeVersion('14.x', true)).toHaveProperty(
|
||||
'major',
|
||||
14
|
||||
@@ -273,24 +265,41 @@ it('should only match supported node versions, otherwise throw an error', async
|
||||
|
||||
it('should match all semver ranges', async () => {
|
||||
// See https://docs.npmjs.com/files/package.json#engines
|
||||
expect(await getSupportedNodeVersion('12.0.0')).toHaveProperty('major', 12);
|
||||
expect(await getSupportedNodeVersion('12.x')).toHaveProperty('major', 12);
|
||||
expect(await getSupportedNodeVersion('14.0.0')).toHaveProperty('major', 14);
|
||||
expect(await getSupportedNodeVersion('14.x')).toHaveProperty('major', 14);
|
||||
expect(await getSupportedNodeVersion('>=10')).toHaveProperty('major', 16);
|
||||
expect(await getSupportedNodeVersion('>=10.3.0')).toHaveProperty('major', 16);
|
||||
expect(await getSupportedNodeVersion('11.5.0 - 12.5.0')).toHaveProperty(
|
||||
expect(await getSupportedNodeVersion('16.5.0 - 16.9.0')).toHaveProperty(
|
||||
'major',
|
||||
12
|
||||
16
|
||||
);
|
||||
expect(await getSupportedNodeVersion('>=9.5.0 <=12.5.0')).toHaveProperty(
|
||||
'major',
|
||||
12
|
||||
);
|
||||
expect(await getSupportedNodeVersion('~12.5.0')).toHaveProperty('major', 12);
|
||||
expect(await getSupportedNodeVersion('^12.5.0')).toHaveProperty('major', 12);
|
||||
expect(await getSupportedNodeVersion('12.5.0 - 14.5.0')).toHaveProperty(
|
||||
expect(await getSupportedNodeVersion('>=9.5.0 <=14.5.0')).toHaveProperty(
|
||||
'major',
|
||||
14
|
||||
);
|
||||
expect(await getSupportedNodeVersion('~14.5.0')).toHaveProperty('major', 14);
|
||||
expect(await getSupportedNodeVersion('^14.5.0')).toHaveProperty('major', 14);
|
||||
expect(await getSupportedNodeVersion('14.5.0 - 14.20.0')).toHaveProperty(
|
||||
'major',
|
||||
14
|
||||
);
|
||||
});
|
||||
|
||||
it('should only allow nodejs18.x when env var is set', async () => {
|
||||
try {
|
||||
expect(getLatestNodeVersion()).toHaveProperty('major', 16);
|
||||
expect(getSupportedNodeVersion('18.x')).rejects.toThrow();
|
||||
|
||||
process.env.VERCEL_ALLOW_NODEJS18 = '1';
|
||||
|
||||
expect(getLatestNodeVersion()).toHaveProperty('major', 18);
|
||||
expect(await getSupportedNodeVersion('18.x')).toHaveProperty('major', 18);
|
||||
expect(await getSupportedNodeVersion('18')).toHaveProperty('major', 18);
|
||||
expect(await getSupportedNodeVersion('18.1.0')).toHaveProperty('major', 18);
|
||||
expect(await getSupportedNodeVersion('>=16')).toHaveProperty('major', 18);
|
||||
} finally {
|
||||
delete process.env.VERCEL_ALLOW_NODEJS18;
|
||||
}
|
||||
});
|
||||
|
||||
it('should ignore node version in vercel dev getNodeVersion()', async () => {
|
||||
@@ -441,6 +450,38 @@ it('should warn for deprecated versions, soon to be discontinued', async () => {
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
|
||||
it('should support initialHeaders and initialStatus correctly', async () => {
|
||||
const { Prerender } = require('@vercel/build-utils/dist/prerender.js');
|
||||
new Prerender({
|
||||
expiration: 1,
|
||||
fallback: null,
|
||||
group: 1,
|
||||
bypassToken: 'some-long-bypass-token-to-make-it-work',
|
||||
initialHeaders: {
|
||||
'content-type': 'application/json',
|
||||
'x-initial': 'true',
|
||||
},
|
||||
initialStatus: 308,
|
||||
});
|
||||
new Prerender({
|
||||
expiration: 1,
|
||||
fallback: null,
|
||||
group: 1,
|
||||
bypassToken: 'some-long-bypass-token-to-make-it-work',
|
||||
initialStatus: 308,
|
||||
});
|
||||
new Prerender({
|
||||
expiration: 1,
|
||||
fallback: null,
|
||||
group: 1,
|
||||
bypassToken: 'some-long-bypass-token-to-make-it-work',
|
||||
initialHeaders: {
|
||||
'content-type': 'application/json',
|
||||
'x-initial': 'true',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should support require by path for legacy builders', () => {
|
||||
const index = require('@vercel/build-utils');
|
||||
|
||||
|
||||
@@ -10,11 +10,9 @@
|
||||
|
||||
## Usage
|
||||
|
||||
Vercel is a platform for **static sites and frontend frameworks**, built to integrate with your headless content, commerce, or database.
|
||||
Vercel is the platform for frontend developers, providing the speed and reliability innovators need to create at the moment of inspiration.
|
||||
|
||||
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.
|
||||
We enable teams to iterate quickly and develop, preview, and ship delightful user experiences. Vercel has zero-configuration support for 35+ frontend frameworks and integrates with your headless content, commerce, or database of choice.
|
||||
|
||||
To install the latest version of Vercel CLI, run this command:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "28.4.5",
|
||||
"version": "28.5.0",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node ./scripts/preinstall.js",
|
||||
"test": "jest --env node --verbose --runInBand --bail --forceExit",
|
||||
"test": "jest --env node --verbose --bail --forceExit",
|
||||
"test-unit": "yarn test test/unit/",
|
||||
"test-integration-cli": "rimraf test/fixtures/integration && ava test/integration.js --serial --fail-fast --verbose",
|
||||
"test-integration-dev": "yarn test test/dev/",
|
||||
@@ -41,16 +41,16 @@
|
||||
"node": ">= 14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "5.5.3",
|
||||
"@vercel/go": "2.2.11",
|
||||
"@vercel/hydrogen": "0.0.24",
|
||||
"@vercel/next": "3.2.1",
|
||||
"@vercel/node": "2.5.21",
|
||||
"@vercel/python": "3.1.20",
|
||||
"@vercel/redwood": "1.0.29",
|
||||
"@vercel/remix": "1.0.30",
|
||||
"@vercel/ruby": "1.3.37",
|
||||
"@vercel/static-build": "1.0.29",
|
||||
"@vercel/build-utils": "5.5.7",
|
||||
"@vercel/go": "2.2.15",
|
||||
"@vercel/hydrogen": "0.0.29",
|
||||
"@vercel/next": "3.2.11",
|
||||
"@vercel/node": "2.6.2",
|
||||
"@vercel/python": "3.1.25",
|
||||
"@vercel/redwood": "1.0.35",
|
||||
"@vercel/remix": "1.0.35",
|
||||
"@vercel/ruby": "1.3.41",
|
||||
"@vercel/static-build": "1.0.36",
|
||||
"update-notifier": "5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -81,7 +81,7 @@
|
||||
"@types/minimatch": "3.0.3",
|
||||
"@types/mri": "1.1.0",
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/node": "11.11.0",
|
||||
"@types/node": "14.18.33",
|
||||
"@types/node-fetch": "2.5.10",
|
||||
"@types/npm-package-arg": "6.1.0",
|
||||
"@types/pluralize": "0.0.29",
|
||||
@@ -95,9 +95,10 @@
|
||||
"@types/which": "1.3.2",
|
||||
"@types/write-json-file": "2.2.1",
|
||||
"@types/yauzl-promise": "2.1.0",
|
||||
"@vercel/client": "12.2.10",
|
||||
"@vercel/frameworks": "1.1.6",
|
||||
"@vercel/fs-detectors": "3.4.1",
|
||||
"@vercel/client": "12.2.17",
|
||||
"@vercel/error-utils": "1.0.3",
|
||||
"@vercel/frameworks": "1.1.12",
|
||||
"@vercel/fs-detectors": "3.5.0",
|
||||
"@vercel/fun": "1.0.4",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@zeit/source-map-support": "0.6.2",
|
||||
@@ -171,7 +172,6 @@
|
||||
"typescript": "4.7.4",
|
||||
"universal-analytics": "0.4.20",
|
||||
"utility-types": "2.1.0",
|
||||
"which": "2.0.2",
|
||||
"write-json-file": "2.2.0",
|
||||
"xdg-app-paths": "5.1.0",
|
||||
"yauzl-promise": "2.1.3"
|
||||
|
||||
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
||||
import dotenv from 'dotenv';
|
||||
import { join, normalize, relative, resolve } from 'path';
|
||||
import {
|
||||
getDiscontinuedNodeVersions,
|
||||
normalizePath,
|
||||
Files,
|
||||
FileFsRef,
|
||||
@@ -57,6 +58,8 @@ import { sortBuilders } from '../util/build/sort-builders';
|
||||
import { toEnumerableError } from '../util/error';
|
||||
import { validateConfig } from '../util/validate-config';
|
||||
|
||||
import { setMonorepoDefaultSettings } from '../util/build/monorepo';
|
||||
|
||||
type BuildResult = BuildResultV2 | BuildResultV3;
|
||||
|
||||
interface SerializedBuilder extends Builder {
|
||||
@@ -98,7 +101,7 @@ const help = () => {
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Build the project
|
||||
${chalk.gray('-')} Build the project
|
||||
|
||||
${chalk.cyan(`$ ${cli.name} build`)}
|
||||
${chalk.cyan(`$ ${cli.name} build --cwd ./path-to-project`)}
|
||||
@@ -270,6 +273,7 @@ async function doBuild(
|
||||
outputDir: string
|
||||
): Promise<void> {
|
||||
const { output } = client;
|
||||
|
||||
const workPath = join(cwd, project.settings.rootDirectory || '.');
|
||||
|
||||
const [pkg, vercelConfig, nowConfig] = await Promise.all([
|
||||
@@ -300,6 +304,8 @@ async function doBuild(
|
||||
...pickOverrides(localConfig),
|
||||
};
|
||||
|
||||
await setMonorepoDefaultSettings(cwd, workPath, projectSettings, output);
|
||||
|
||||
// Get a list of source files
|
||||
const files = (await getFiles(workPath, client)).map(f =>
|
||||
normalizePath(relative(workPath, f))
|
||||
@@ -467,6 +473,25 @@ async function doBuild(
|
||||
);
|
||||
const buildResult = await builder.build(buildOptions);
|
||||
|
||||
if (
|
||||
buildResult &&
|
||||
'output' in buildResult &&
|
||||
'runtime' in buildResult.output &&
|
||||
'type' in buildResult.output &&
|
||||
buildResult.output.type === 'Lambda'
|
||||
) {
|
||||
const lambdaRuntime = buildResult.output.runtime;
|
||||
if (
|
||||
getDiscontinuedNodeVersions().some(o => o.runtime === lambdaRuntime)
|
||||
) {
|
||||
throw new NowBuildError({
|
||||
code: 'NODEJS_DISCONTINUED_VERSION',
|
||||
message: `The Runtime "${build.use}" is using "${lambdaRuntime}", which is discontinued. Please upgrade your Runtime to a more recent version or consult the author for more details.`,
|
||||
link: 'https://github.com/vercel/vercel/blob/main/DEVELOPING_A_RUNTIME.md#lambdaruntime',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Store the build result to generate the final `config.json` after
|
||||
// all builds have completed
|
||||
buildResults.set(build, buildResult);
|
||||
|
||||
@@ -70,7 +70,7 @@ import getPrebuiltJson from '../../util/deploy/get-prebuilt-json';
|
||||
import { createGitMeta } from '../../util/create-git-meta';
|
||||
import { isValidArchive } from '../../util/deploy/validate-archive-format';
|
||||
import { parseEnv } from '../../util/parse-env';
|
||||
import { errorToString, isErrnoException, isError } from '../../util/is-error';
|
||||
import { errorToString, isErrnoException, isError } from '@vercel/error-utils';
|
||||
import { pickOverrides } from '../../util/projects/project-settings';
|
||||
|
||||
export default async (client: Client): Promise<number> => {
|
||||
|
||||
@@ -3,16 +3,14 @@ import fs from 'fs-extra';
|
||||
|
||||
import DevServer from '../../util/dev/server';
|
||||
import { parseListen } from '../../util/dev/parse-listen';
|
||||
import { ProjectEnvVariable } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import { getLinkedProject } from '../../util/projects/link';
|
||||
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';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import param from '../../util/output/param';
|
||||
import { OUTPUT_DIR } from '../../util/build/write-build-result';
|
||||
import { pullEnvRecords } from '../../util/env/get-env-records';
|
||||
|
||||
type Options = {
|
||||
'--listen': string;
|
||||
@@ -57,8 +55,7 @@ export default async function dev(
|
||||
}
|
||||
|
||||
let projectSettings: ProjectSettings | undefined;
|
||||
let projectEnvs: ProjectEnvVariable[] = [];
|
||||
let systemEnvValues: string[] = [];
|
||||
let envValues: Record<string, string> = {};
|
||||
if (link.status === 'linked') {
|
||||
const { project, org } = link;
|
||||
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
@@ -69,19 +66,15 @@ export default async function dev(
|
||||
cwd = join(cwd, project.rootDirectory);
|
||||
}
|
||||
|
||||
[{ envs: projectEnvs }, { systemEnvValues }] = await Promise.all([
|
||||
getDecryptedEnvRecords(output, client, project.id, 'vercel-cli:dev'),
|
||||
project.autoExposeSystemEnvs
|
||||
? getSystemEnvValues(output, client, project.id)
|
||||
: { systemEnvValues: [] },
|
||||
]);
|
||||
envValues = (
|
||||
await pullEnvRecords(output, client, project.id, 'vercel-cli:dev')
|
||||
).env;
|
||||
}
|
||||
|
||||
const devServer = new DevServer(cwd, {
|
||||
output,
|
||||
projectSettings,
|
||||
projectEnvs,
|
||||
systemEnvValues,
|
||||
envValues,
|
||||
});
|
||||
|
||||
// If there is no Development Command, we must delete the
|
||||
|
||||
@@ -15,7 +15,7 @@ import readConfig from '../../util/config/read-config';
|
||||
import readJSONFile from '../../util/read-json-file';
|
||||
import { getPkgName, getCommandName } from '../../util/pkg-name';
|
||||
import { CantParseJSONFile } from '../../util/errors-ts';
|
||||
import { isErrnoException } from '../../util/is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
dev: ['dev'],
|
||||
|
||||
@@ -11,7 +11,7 @@ import promptBool from '../../util/input/prompt-bool';
|
||||
import purchaseDomain from '../../util/domains/purchase-domain';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { errorToString } from '../../util/is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
|
||||
type Options = {};
|
||||
|
||||
|
||||
29
packages/cli/src/commands/env/pull.ts
vendored
29
packages/cli/src/commands/env/pull.ts
vendored
@@ -4,21 +4,21 @@ import { closeSync, openSync, readSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { Project, ProjectEnvTarget } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import exposeSystemEnvs from '../../util/dev/expose-system-envs';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import getSystemEnvValues from '../../util/env/get-system-env-values';
|
||||
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import { Output } from '../../util/output';
|
||||
import param from '../../util/output/param';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { EnvRecordsSource } from '../../util/env/get-env-records';
|
||||
import {
|
||||
EnvRecordsSource,
|
||||
pullEnvRecords,
|
||||
} from '../../util/env/get-env-records';
|
||||
import {
|
||||
buildDeltaString,
|
||||
createEnvObject,
|
||||
} from '../../util/env/diff-env-files';
|
||||
import { isErrnoException } from '../../util/is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
|
||||
|
||||
@@ -97,20 +97,11 @@ export default async function pull(
|
||||
const pullStamp = stamp();
|
||||
output.spinner('Downloading');
|
||||
|
||||
const [{ envs: projectEnvs }, { systemEnvValues }] = await Promise.all([
|
||||
getDecryptedEnvRecords(output, client, project.id, source, environment),
|
||||
project.autoExposeSystemEnvs
|
||||
? getSystemEnvValues(output, client, project.id)
|
||||
: { systemEnvValues: [] },
|
||||
]);
|
||||
|
||||
const records = exposeSystemEnvs(
|
||||
projectEnvs,
|
||||
systemEnvValues,
|
||||
project.autoExposeSystemEnvs,
|
||||
undefined,
|
||||
environment
|
||||
);
|
||||
const records = (
|
||||
await pullEnvRecords(output, client, project.id, source, {
|
||||
target: environment || ProjectEnvTarget.Development,
|
||||
})
|
||||
).env;
|
||||
|
||||
let deltaString = '';
|
||||
let oldEnv;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../../util/client';
|
||||
import { ensureLink } from '../../util/ensure-link';
|
||||
import { ensureLink } from '../../util/link/ensure-link';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
|
||||
import handleError from '../../util/handle-error';
|
||||
@@ -80,7 +80,7 @@ export default async function main(client: Client) {
|
||||
argv._ = argv._.slice(1);
|
||||
subcommand = argv._[0];
|
||||
const args = argv._.slice(1);
|
||||
const confirm = Boolean(argv['--yes']);
|
||||
const autoConfirm = Boolean(argv['--yes']);
|
||||
const { output } = client;
|
||||
|
||||
let paths = [process.cwd()];
|
||||
@@ -90,7 +90,7 @@ export default async function main(client: Client) {
|
||||
}
|
||||
const { path } = pathValidation;
|
||||
|
||||
const linkedProject = await ensureLink('git', client, path, confirm);
|
||||
const linkedProject = await ensureLink('git', client, path, { autoConfirm });
|
||||
if (typeof linkedProject === 'number') {
|
||||
return linkedProject;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import init from './init';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import { isError } from '../../util/is-error';
|
||||
import { isError } from '@vercel/error-utils';
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
init: ['init'],
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getDeployment } from '../util/get-deployment';
|
||||
import { Deployment } from '@vercel/client';
|
||||
import { Build } from '../types';
|
||||
import title from 'title';
|
||||
import { isErrnoException } from '../util/is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { URL } from 'url';
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ import Client from '../../util/client';
|
||||
import getArgs from '../../util/get-args';
|
||||
import logo from '../../util/output/logo';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import setupAndLink from '../../util/link/setup-and-link';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import param from '../../util/output/param';
|
||||
import { ensureLink } from '../../util/link/ensure-link';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -70,31 +68,16 @@ export default async function main(client: Client) {
|
||||
}
|
||||
|
||||
const cwd = argv._[1] || process.cwd();
|
||||
const link = await setupAndLink(client, cwd, {
|
||||
|
||||
const link = await ensureLink('link', client, cwd, {
|
||||
autoConfirm: !!argv['--yes'],
|
||||
forceDelete: true,
|
||||
autoConfirm: argv['--yes'],
|
||||
projectName: argv['--project'],
|
||||
successEmoji: 'success',
|
||||
setupMsg: 'Set up',
|
||||
});
|
||||
|
||||
if (link.status === 'error') {
|
||||
if (link.reason === 'HEADLESS') {
|
||||
client.output.error(
|
||||
`Command ${getCommandName(
|
||||
'link'
|
||||
)} requires confirmation. Use option ${param('--yes')} to confirm.`
|
||||
);
|
||||
}
|
||||
return link.exitCode;
|
||||
} else if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
} else if (link.status === 'linked') {
|
||||
// Successfully linked
|
||||
return 0;
|
||||
} else {
|
||||
const err: never = link;
|
||||
throw new Error('Unknown link status: ' + err);
|
||||
if (typeof link === 'number') {
|
||||
return link;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ import Client from '../util/client';
|
||||
import { Deployment } from '@vercel/client';
|
||||
import validatePaths from '../util/validate-paths';
|
||||
import { getLinkedProject } from '../util/projects/link';
|
||||
import { ensureLink } from '../util/ensure-link';
|
||||
import { ensureLink } from '../util/link/ensure-link';
|
||||
import getScope from '../util/get-scope';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { isErrnoException } from '../util/is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -56,7 +56,7 @@ const help = () => {
|
||||
${chalk.gray('–')} List all deployments for the project ${chalk.dim(
|
||||
'`my-app`'
|
||||
)} in the team of the currently linked project
|
||||
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} ls my-app`)}
|
||||
|
||||
${chalk.gray('–')} Filter deployments by metadata
|
||||
@@ -112,7 +112,7 @@ export default async function main(client: Client) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
const yes = !!argv['--yes'];
|
||||
const autoConfirm = !!argv['--yes'];
|
||||
const prod = argv['--prod'] || false;
|
||||
|
||||
const meta = parseMeta(argv['--meta']);
|
||||
@@ -145,7 +145,9 @@ export default async function main(client: Client) {
|
||||
// If there's no linked project and user doesn't pass `app` arg,
|
||||
// prompt to link their current directory.
|
||||
if (status === 'not_linked' && !app) {
|
||||
const linkedProject = await ensureLink('list', client, path, yes);
|
||||
const linkedProject = await ensureLink('list', client, path, {
|
||||
autoConfirm,
|
||||
});
|
||||
if (typeof linkedProject === 'number') {
|
||||
return linkedProject;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import getArgs from '../util/get-args';
|
||||
import Client from '../util/client';
|
||||
import { getCommandName, getPkgName } from '../util/pkg-name';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { errorToString } from '../util/is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
|
||||
@@ -4,24 +4,18 @@ import Client from '../util/client';
|
||||
import { ProjectEnvTarget } from '../types';
|
||||
import { emoji, prependEmoji } from '../util/emoji';
|
||||
import getArgs from '../util/get-args';
|
||||
import setupAndLink from '../util/link/setup-and-link';
|
||||
import logo from '../util/output/logo';
|
||||
import stamp from '../util/output/stamp';
|
||||
import { getPkgName } from '../util/pkg-name';
|
||||
import {
|
||||
getLinkedProject,
|
||||
VERCEL_DIR,
|
||||
VERCEL_DIR_PROJECT,
|
||||
} from '../util/projects/link';
|
||||
import { VERCEL_DIR, VERCEL_DIR_PROJECT } from '../util/projects/link';
|
||||
import { writeProjectSettings } from '../util/projects/project-settings';
|
||||
import envPull from './env/pull';
|
||||
import { getCommandName } from '../util/pkg-name';
|
||||
import param from '../util/output/param';
|
||||
import type { Project, Org } from '../types';
|
||||
import type { Project } from '../types';
|
||||
import {
|
||||
isValidEnvTarget,
|
||||
getEnvTargetPlaceholder,
|
||||
} from '../util/env/env-target';
|
||||
import { ensureLink } from '../util/link/ensure-link';
|
||||
|
||||
const help = () => {
|
||||
return console.log(`
|
||||
@@ -83,43 +77,6 @@ function parseArgs(client: Client) {
|
||||
return argv;
|
||||
}
|
||||
|
||||
type LinkResult = {
|
||||
org: Org;
|
||||
project: Project;
|
||||
};
|
||||
async function ensureLink(
|
||||
client: Client,
|
||||
cwd: string,
|
||||
yes: boolean
|
||||
): Promise<LinkResult | number> {
|
||||
let link = await getLinkedProject(client, cwd);
|
||||
if (link.status === 'not_linked') {
|
||||
link = await setupAndLink(client, cwd, {
|
||||
autoConfirm: yes,
|
||||
successEmoji: 'link',
|
||||
setupMsg: 'Set up',
|
||||
});
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (link.status === 'error') {
|
||||
if (link.reason === 'HEADLESS') {
|
||||
client.output.error(
|
||||
`Command ${getCommandName(
|
||||
'pull'
|
||||
)} requires confirmation. Use option ${param('--yes')} to confirm.`
|
||||
);
|
||||
}
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
return { org: link.org, project: link.project };
|
||||
}
|
||||
|
||||
async function pullAllEnvFiles(
|
||||
environment: ProjectEnvTarget,
|
||||
client: Client,
|
||||
@@ -140,7 +97,9 @@ async function pullAllEnvFiles(
|
||||
);
|
||||
}
|
||||
|
||||
function parseEnvironment(environment = 'development'): ProjectEnvTarget {
|
||||
export function parseEnvironment(
|
||||
environment = 'development'
|
||||
): ProjectEnvTarget {
|
||||
if (!isValidEnvTarget(environment)) {
|
||||
throw new Error(
|
||||
`environment "${environment}" not supported; must be one of ${getEnvTargetPlaceholder()}`
|
||||
@@ -156,10 +115,10 @@ export default async function main(client: Client) {
|
||||
}
|
||||
|
||||
const cwd = argv._[1] || process.cwd();
|
||||
const yes = Boolean(argv['--yes']);
|
||||
const autoConfirm = Boolean(argv['--yes']);
|
||||
const environment = parseEnvironment(argv['--environment'] || undefined);
|
||||
|
||||
const link = await ensureLink(client, cwd, yes);
|
||||
const link = await ensureLink('pull', client, cwd, { autoConfirm });
|
||||
if (typeof link === 'number') {
|
||||
return link;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getPkgName, getCommandName } from '../../util/pkg-name';
|
||||
import Client from '../../util/client';
|
||||
import createTeam from '../../util/teams/create-team';
|
||||
import patchTeam from '../../util/teams/patch-team';
|
||||
import { errorToString, isError } from '../../util/is-error';
|
||||
import { errorToString, isError } from '@vercel/error-utils';
|
||||
|
||||
const validateSlugKeypress = (data: string, value: string) =>
|
||||
// TODO: the `value` here should contain the current value + the keypress
|
||||
|
||||
@@ -12,7 +12,7 @@ import { email as regexEmail } from '../../util/input/regexes';
|
||||
import getTeams from '../../util/teams/get-teams';
|
||||
import inviteUserToTeam from '../../util/teams/invite-user-to-team';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
import { errorToString, isError } from '../../util/is-error';
|
||||
import { errorToString, isError } from '@vercel/error-utils';
|
||||
|
||||
const validateEmail = (data: string) =>
|
||||
regexEmail.test(data.trim()) || data.length === 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { isErrnoException, isError, errorToString } from './util/is-error';
|
||||
import { isErrnoException, isError, errorToString } from '@vercel/error-utils';
|
||||
|
||||
try {
|
||||
// Test to see if cwd has been deleted before
|
||||
@@ -610,6 +610,21 @@ const main = async () => {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isErrnoException(err) && err.code === 'ECONNRESET') {
|
||||
// Error message will look like the following:
|
||||
// request to https://api.vercel.com/v2/user failed, reason: socket hang up
|
||||
const matches = /request to https:\/\/(.*?)\//.exec(err.message || '');
|
||||
const hostname = matches?.[1];
|
||||
if (hostname) {
|
||||
output.error(
|
||||
`Connection to ${highlight(
|
||||
hostname
|
||||
)} interrupted. Please verify your internet connectivity and DNS configuration.`
|
||||
);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
isErrnoException(err) &&
|
||||
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
|
||||
|
||||
@@ -16,7 +16,7 @@ import { VERCEL_DIR } from '../projects/link';
|
||||
import { Output } from '../output';
|
||||
import readJSONFile from '../read-json-file';
|
||||
import { CantParseJSONFile } from '../errors-ts';
|
||||
import { errorToString, isErrnoException, isError } from '../is-error';
|
||||
import { errorToString, isErrnoException, isError } from '@vercel/error-utils';
|
||||
import cmd from '../output/cmd';
|
||||
import code from '../output/code';
|
||||
|
||||
|
||||
139
packages/cli/src/util/build/monorepo.ts
Normal file
139
packages/cli/src/util/build/monorepo.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import fs from 'fs-extra';
|
||||
import { join, relative, basename } from 'path';
|
||||
import {
|
||||
detectFramework,
|
||||
monorepoManagers,
|
||||
LocalFileSystemDetector,
|
||||
packageManagers,
|
||||
} from '@vercel/fs-detectors';
|
||||
import { ProjectLinkAndSettings } from '../projects/project-settings';
|
||||
import { Output } from '../output';
|
||||
import title from 'title';
|
||||
|
||||
export async function setMonorepoDefaultSettings(
|
||||
cwd: string,
|
||||
workPath: string,
|
||||
projectSettings: ProjectLinkAndSettings['settings'],
|
||||
output: Output
|
||||
) {
|
||||
const localFileSystem = new LocalFileSystemDetector(cwd);
|
||||
|
||||
const [monorepoManager, packageManager] = await Promise.all([
|
||||
detectFramework({
|
||||
fs: localFileSystem,
|
||||
frameworkList: monorepoManagers,
|
||||
}),
|
||||
detectFramework({
|
||||
fs: localFileSystem,
|
||||
frameworkList: packageManagers,
|
||||
}),
|
||||
]);
|
||||
|
||||
const projectName = basename(workPath);
|
||||
const relativeToRoot = relative(workPath, cwd);
|
||||
|
||||
const setCommand = (
|
||||
command: 'buildCommand' | 'installCommand',
|
||||
value: string
|
||||
) => {
|
||||
if (projectSettings[command]) {
|
||||
output.warn(
|
||||
`Cannot automatically assign ${command} as it is already set via project settings or configuarion overrides.`
|
||||
);
|
||||
} else {
|
||||
projectSettings[command] = value;
|
||||
}
|
||||
};
|
||||
|
||||
if (monorepoManager) {
|
||||
output.log(
|
||||
`Automatically detected ${title(
|
||||
monorepoManager
|
||||
)} monorepo manager. Attempting to assign default \`buildCommand\` and \`installCommand\` settings.`
|
||||
);
|
||||
}
|
||||
|
||||
if (monorepoManager === 'turbo') {
|
||||
// No ENOENT handling required here since conditional wouldn't be `true` unless `turbo.json` was found.
|
||||
const turboJSON = JSON.parse(
|
||||
fs.readFileSync(join(cwd, 'turbo.json'), 'utf-8')
|
||||
);
|
||||
|
||||
if (!turboJSON?.pipeline?.build) {
|
||||
output.warn(
|
||||
'Missing required `build` pipeline in turbo.json. Skipping automatic setting assignment.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setCommand(
|
||||
'buildCommand',
|
||||
`cd ${relativeToRoot} && npx turbo run build --filter=${projectName}...`
|
||||
);
|
||||
setCommand(
|
||||
'installCommand',
|
||||
`cd ${relativeToRoot} && ${packageManager} install`
|
||||
);
|
||||
} else if (monorepoManager === 'nx') {
|
||||
// No ENOENT handling required here since conditional wouldn't be `true` unless `nx.json` was found.
|
||||
const nxJSON = JSON.parse(fs.readFileSync(join(cwd, 'nx.json'), 'utf-8'));
|
||||
|
||||
if (!nxJSON?.targetDefaults?.build) {
|
||||
output.log(
|
||||
'Missing default `build` target in nx.json. Checking for project level Nx configuration...'
|
||||
);
|
||||
|
||||
const [projectJSONBuf, packageJsonBuf] = await Promise.all([
|
||||
fs.readFile(join(workPath, 'project.json')).catch(() => null),
|
||||
fs.readFile(join(workPath, 'package.json')).catch(() => null),
|
||||
]);
|
||||
|
||||
let hasBuildTarget = false;
|
||||
|
||||
if (projectJSONBuf) {
|
||||
output.log('Found project.json Nx configuration.');
|
||||
const projectJSON = JSON.parse(projectJSONBuf.toString('utf-8'));
|
||||
if (projectJSON?.targets?.build) {
|
||||
hasBuildTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (packageJsonBuf) {
|
||||
const packageJSON = JSON.parse(packageJsonBuf.toString('utf-8'));
|
||||
if (packageJSON?.nx) {
|
||||
output.log('Found package.json Nx configuration.');
|
||||
if (packageJSON.nx.targets?.build) {
|
||||
hasBuildTarget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasBuildTarget) {
|
||||
output.warn(
|
||||
'Missing required `build` target in either project.json or package.json Nx configuration. Skipping automatic setting assignment.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setCommand(
|
||||
'buildCommand',
|
||||
`cd ${relativeToRoot} && npx nx build ${projectName}`
|
||||
);
|
||||
setCommand(
|
||||
'installCommand',
|
||||
`cd ${relativeToRoot} && ${packageManager} install`
|
||||
);
|
||||
}
|
||||
// TODO (@Ethan-Arrowood) - Revisit rush support when we can test it better
|
||||
/* else if (monorepoManager === 'rush') {
|
||||
setCommand(
|
||||
'buildCommand',
|
||||
`node ${relativeToRoot}/common/scripts/install-run-rush.js build --to ${projectName}`
|
||||
);
|
||||
setCommand(
|
||||
'installCommand',
|
||||
`node ${relativeToRoot}/common/scripts/install-run-rush.js install`
|
||||
);
|
||||
} */
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import Client from '../client';
|
||||
import { Cert } from '../../types';
|
||||
import { isErrnoException } from '../is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
|
||||
export default async function createCertFromFile(
|
||||
|
||||
@@ -2,7 +2,7 @@ import retry from 'async-retry';
|
||||
import { Cert } from '../../types';
|
||||
import Client from '../client';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
import { isError } from '../is-error';
|
||||
import { isError } from '@vercel/error-utils';
|
||||
|
||||
// When it's a configuration error we should retry because of the DNS propagation
|
||||
// otherwise we bail to handle the error in the upper level
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { bold } from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { EventEmitter } from 'events';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { parse as parseUrl } from 'url';
|
||||
import { URL } from 'url';
|
||||
import { VercelConfig } from '@vercel/client';
|
||||
import retry, { RetryFunction, Options as RetryOptions } from 'async-retry';
|
||||
import fetch, { BodyInit, Headers, RequestInit, Response } from 'node-fetch';
|
||||
@@ -23,7 +22,7 @@ import type {
|
||||
} from '../types';
|
||||
import { sharedPromise } from './promise';
|
||||
import { APIError } from './errors-ts';
|
||||
import { normalizeError } from './is-error';
|
||||
import { normalizeError } from '@vercel/error-utils';
|
||||
|
||||
const isSAMLError = (v: any): v is SAMLError => {
|
||||
return v && v.saml;
|
||||
@@ -87,25 +86,18 @@ export default class Client extends EventEmitter implements Stdio {
|
||||
}
|
||||
|
||||
private _fetch(_url: string, opts: FetchOptions = {}) {
|
||||
const parsedUrl = parseUrl(_url, true);
|
||||
const apiUrl = parsedUrl.host
|
||||
? `${parsedUrl.protocol}//${parsedUrl.host}`
|
||||
: '';
|
||||
const url = new URL(_url, this.apiUrl);
|
||||
|
||||
if (opts.accountId || opts.useCurrentTeam !== false) {
|
||||
const query = new URLSearchParams(parsedUrl.query);
|
||||
|
||||
if (opts.accountId) {
|
||||
if (opts.accountId.startsWith('team_')) {
|
||||
query.set('teamId', opts.accountId);
|
||||
url.searchParams.set('teamId', opts.accountId);
|
||||
} else {
|
||||
query.delete('teamId');
|
||||
url.searchParams.delete('teamId');
|
||||
}
|
||||
} else if (opts.useCurrentTeam !== false && this.config.currentTeam) {
|
||||
query.set('teamId', this.config.currentTeam);
|
||||
url.searchParams.set('teamId', this.config.currentTeam);
|
||||
}
|
||||
|
||||
_url = `${apiUrl}${parsedUrl.pathname}?${query}`;
|
||||
}
|
||||
|
||||
const headers = new Headers(opts.headers);
|
||||
@@ -122,7 +114,6 @@ export default class Client extends EventEmitter implements Stdio {
|
||||
body = opts.body;
|
||||
}
|
||||
|
||||
const url = `${apiUrl ? '' : this.apiUrl}${_url}`;
|
||||
const requestId = this.requestIdCounter++;
|
||||
return this.output.time(res => {
|
||||
if (res) {
|
||||
@@ -130,7 +121,7 @@ export default class Client extends EventEmitter implements Stdio {
|
||||
res.statusText
|
||||
}: ${res.headers.get('x-vercel-id')}`;
|
||||
} else {
|
||||
return `#${requestId} → ${opts.method || 'GET'} ${url}`;
|
||||
return `#${requestId} → ${opts.method || 'GET'} ${url.href}`;
|
||||
}
|
||||
}, fetch(url, { ...opts, headers, body }));
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import error from '../output/error';
|
||||
import highlight from '../output/highlight';
|
||||
import { VercelConfig } from '../dev/types';
|
||||
import { AuthConfig, GlobalConfig } from '../../types';
|
||||
import { isErrnoException, isError } from '../is-error';
|
||||
import { isErrnoException, isError } from '@vercel/error-utils';
|
||||
|
||||
const VERCEL_DIR = getGlobalPathConfig();
|
||||
const CONFIG_FILE_PATH = join(VERCEL_DIR, 'config.json');
|
||||
|
||||
@@ -5,7 +5,7 @@ import git from 'git-last-commit';
|
||||
import { exec } from 'child_process';
|
||||
import { GitMetadata, Project } from '../types';
|
||||
import { Output } from './output';
|
||||
import { errorToString } from './is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
|
||||
export async function createGitMeta(
|
||||
directory: string,
|
||||
|
||||
@@ -115,29 +115,39 @@ export default async function processDeployment({
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
const totalSizeHuman = bytes.format(missingSize, { decimalPlaces: 1 });
|
||||
|
||||
uploads.forEach((e: any) =>
|
||||
e.on('progress', () => {
|
||||
const uploadedBytes = uploads.reduce((acc: number, e: any) => {
|
||||
return acc + e.bytesUploaded;
|
||||
}, 0);
|
||||
// When stderr is not a TTY then we only want to
|
||||
// print upload progress in 25% increments
|
||||
let nextStep = 0;
|
||||
const stepSize = now._client.stderr.isTTY ? 0 : 0.25;
|
||||
|
||||
const bar = progress(uploadedBytes, missingSize);
|
||||
if (!bar || uploadedBytes === missingSize) {
|
||||
output.spinner(deployingSpinnerVal, 0);
|
||||
} else {
|
||||
const uploadedHuman = bytes.format(uploadedBytes, {
|
||||
decimalPlaces: 1,
|
||||
fixedDecimals: true,
|
||||
});
|
||||
const updateProgress = () => {
|
||||
const uploadedBytes = uploads.reduce((acc: number, e: any) => {
|
||||
return acc + e.bytesUploaded;
|
||||
}, 0);
|
||||
|
||||
const bar = progress(uploadedBytes, missingSize);
|
||||
if (!bar) {
|
||||
output.spinner(deployingSpinnerVal, 0);
|
||||
} else {
|
||||
const uploadedHuman = bytes.format(uploadedBytes, {
|
||||
decimalPlaces: 1,
|
||||
fixedDecimals: true,
|
||||
});
|
||||
const percent = uploadedBytes / missingSize;
|
||||
if (percent >= nextStep) {
|
||||
output.spinner(
|
||||
`Uploading ${chalk.reset(
|
||||
`[${bar}] (${uploadedHuman}/${totalSizeHuman})`
|
||||
)}`,
|
||||
0
|
||||
);
|
||||
nextStep += stepSize;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
uploads.forEach((e: any) => e.on('progress', updateProgress));
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
if (event.type === 'file-uploaded') {
|
||||
|
||||
@@ -87,8 +87,12 @@ async function createBuildProcess(
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// The first message that the builder process sends is the `ready` event
|
||||
buildProcess.once('message', ({ type }) => {
|
||||
if (type !== 'ready') {
|
||||
buildProcess.once('message', data => {
|
||||
if (
|
||||
data !== null &&
|
||||
typeof data === 'object' &&
|
||||
(data as { type: string }).type !== 'ready'
|
||||
) {
|
||||
reject(new Error('Did not get "ready" event from builder'));
|
||||
} else {
|
||||
resolve(buildProcess);
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
ProjectEnvType,
|
||||
ProjectEnvVariable,
|
||||
ProjectEnvTarget,
|
||||
} from '../../types';
|
||||
import { Env } from '@vercel/build-utils';
|
||||
|
||||
function getSystemEnvValue(
|
||||
systemEnvRef: string,
|
||||
{ vercelUrl }: { vercelUrl?: string }
|
||||
) {
|
||||
if (systemEnvRef === 'VERCEL_URL') {
|
||||
return vercelUrl || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export default function exposeSystemEnvs(
|
||||
projectEnvs: ProjectEnvVariable[],
|
||||
systemEnvValues: string[],
|
||||
autoExposeSystemEnvs: boolean | undefined,
|
||||
vercelUrl?: string,
|
||||
target?: ProjectEnvTarget
|
||||
) {
|
||||
const envs: Env = {};
|
||||
|
||||
if (autoExposeSystemEnvs) {
|
||||
envs['VERCEL'] = '1';
|
||||
envs['VERCEL_ENV'] = target || 'development';
|
||||
|
||||
for (const key of systemEnvValues) {
|
||||
envs[key] = getSystemEnvValue(key, { vercelUrl });
|
||||
}
|
||||
}
|
||||
|
||||
for (let env of projectEnvs) {
|
||||
if (env.type === ProjectEnvType.System) {
|
||||
envs[env.key] = getSystemEnvValue(env.value, { vercelUrl });
|
||||
} else {
|
||||
envs[env.key] = env.value;
|
||||
}
|
||||
}
|
||||
|
||||
return envs;
|
||||
}
|
||||
@@ -16,3 +16,75 @@ export function nodeHeadersToFetchHeaders(
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request headers that are not allowed to be overridden by a middleware.
|
||||
*/
|
||||
const NONOVERRIDABLE_HEADERS: Set<string> = new Set([
|
||||
'host',
|
||||
'connection',
|
||||
'content-length',
|
||||
'transfer-encoding',
|
||||
'keep-alive',
|
||||
'transfer-encoding',
|
||||
'te',
|
||||
'upgrade',
|
||||
'trailer',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Adds/Updates/Deletes headers in `reqHeaders` based on the response headers
|
||||
* from a middleware (`respHeaders`).
|
||||
*
|
||||
* `x-middleware-override-headers` is a comma-separated list of *all* header
|
||||
* names that should appear in new request headers. Names not in this list
|
||||
* will be deleted.
|
||||
*
|
||||
* `x-middleware-request-*` is the new value for each header. This can't be
|
||||
* omitted, even if the header is not being modified.
|
||||
*
|
||||
*/
|
||||
export function applyOverriddenHeaders(
|
||||
reqHeaders: { [k: string]: string | string[] | undefined },
|
||||
respHeaders: Headers
|
||||
) {
|
||||
const overriddenHeaders = respHeaders.get('x-middleware-override-headers');
|
||||
if (!overriddenHeaders) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overriddenKeys: Set<string> = new Set();
|
||||
for (const key of overriddenHeaders.split(',')) {
|
||||
overriddenKeys.add(key.trim());
|
||||
}
|
||||
|
||||
respHeaders.delete('x-middleware-override-headers');
|
||||
|
||||
// Delete headers.
|
||||
for (const key of Object.keys(reqHeaders)) {
|
||||
if (!NONOVERRIDABLE_HEADERS.has(key) && !overriddenKeys.has(key)) {
|
||||
delete reqHeaders[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add headers.
|
||||
for (const key of overriddenKeys.keys()) {
|
||||
if (NONOVERRIDABLE_HEADERS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const valueKey = 'x-middleware-request-' + key;
|
||||
const newValue = respHeaders.get(valueKey);
|
||||
const oldValue = reqHeaders[key];
|
||||
|
||||
if (oldValue !== newValue) {
|
||||
if (newValue) {
|
||||
reqHeaders[key] = newValue;
|
||||
} else {
|
||||
delete reqHeaders[key];
|
||||
}
|
||||
}
|
||||
|
||||
respHeaders.delete(valueKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ export function parseListen(str: string, defaultPort = 3000): ListenSpec {
|
||||
return [url.pathname];
|
||||
case 'tcp:':
|
||||
url.port = url.port || String(defaultPort);
|
||||
return [parseInt(url.port, 10), url.hostname];
|
||||
return [parseInt(url.port, 10), url.hostname ?? undefined];
|
||||
default:
|
||||
if (!url.slashes) {
|
||||
if (url.protocol === null) {
|
||||
return [defaultPort, url.pathname];
|
||||
return [defaultPort, url.pathname ?? undefined];
|
||||
}
|
||||
port = Number(url.hostname);
|
||||
if (url.protocol && !isNaN(port)) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @param querystring - The querystring to parse, also known as the "search" string.
|
||||
*/
|
||||
export function parseQueryString(
|
||||
querystring?: string
|
||||
querystring?: string | null
|
||||
): Record<string, string[]> {
|
||||
const query: Record<string, string[]> = Object.create(null);
|
||||
if (!querystring || !querystring.startsWith('?') || querystring === '?') {
|
||||
@@ -38,9 +38,9 @@ export function parseQueryString(
|
||||
*/
|
||||
export function formatQueryString(
|
||||
query: Record<string, string[]> | undefined
|
||||
): string | undefined {
|
||||
): string | null {
|
||||
if (!query) {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
let s = '';
|
||||
let prefix = '?';
|
||||
@@ -55,5 +55,5 @@ export function formatQueryString(
|
||||
prefix = '&';
|
||||
}
|
||||
}
|
||||
return s || undefined;
|
||||
return s || null;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ export async function devRouter(
|
||||
phase?: HandleValue | null
|
||||
): Promise<RouteResult> {
|
||||
let result: RouteResult | undefined;
|
||||
let { pathname: reqPathname = '/', search: reqSearch } = url.parse(reqUrl);
|
||||
let { pathname: reqPathname, search: reqSearch } = url.parse(reqUrl);
|
||||
reqPathname ??= '/';
|
||||
const reqQuery = parseQueryString(reqSearch);
|
||||
const combinedHeaders: HttpHeadersConfig = { ...previousHeaders };
|
||||
let status: number | undefined;
|
||||
@@ -130,7 +131,8 @@ export async function devRouter(
|
||||
phase !== 'hit' &&
|
||||
!isDestUrl
|
||||
) {
|
||||
const { pathname = '/' } = url.parse(destPath);
|
||||
let { pathname } = url.parse(destPath);
|
||||
pathname ??= '/';
|
||||
const hasDestFile = await devServer.hasFilesystem(
|
||||
pathname,
|
||||
vercelConfig
|
||||
@@ -186,8 +188,9 @@ export async function devRouter(
|
||||
if (!destPath.startsWith('/')) {
|
||||
destPath = `/${destPath}`;
|
||||
}
|
||||
const { pathname: destPathname = '/', search: destSearch } =
|
||||
let { pathname: destPathname, search: destSearch } =
|
||||
url.parse(destPath);
|
||||
destPathname ??= '/';
|
||||
const destQuery = parseQueryString(destSearch);
|
||||
Object.assign(destQuery, reqQuery);
|
||||
result = {
|
||||
|
||||
@@ -18,7 +18,6 @@ import directoryTemplate from 'serve-handler/src/directory';
|
||||
import getPort from 'get-port';
|
||||
import isPortReachable from 'is-port-reachable';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import which from 'which';
|
||||
import npa from 'npm-package-arg';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
Builder,
|
||||
cloneEnv,
|
||||
Env,
|
||||
getNodeBinPath,
|
||||
StartDevServerResult,
|
||||
FileFsRef,
|
||||
PackageJson,
|
||||
@@ -85,17 +85,16 @@ import {
|
||||
HttpHeadersConfig,
|
||||
EnvConfigs,
|
||||
} from './types';
|
||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
import exposeSystemEnvs from './expose-system-envs';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import { treeKill } from '../tree-kill';
|
||||
import { nodeHeadersToFetchHeaders } from './headers';
|
||||
import { applyOverriddenHeaders, nodeHeadersToFetchHeaders } from './headers';
|
||||
import { formatQueryString, parseQueryString } from './parse-query-string';
|
||||
import {
|
||||
errorToString,
|
||||
isErrnoException,
|
||||
isError,
|
||||
isSpawnError,
|
||||
} from '../is-error';
|
||||
} from '@vercel/error-utils';
|
||||
import isURL from './is-url';
|
||||
import { pickOverrides } from '../projects/project-settings';
|
||||
import { replaceLocalhost } from './parse-listen';
|
||||
@@ -168,15 +167,13 @@ export default class DevServer {
|
||||
private blockingBuildsPromise: Promise<void> | null;
|
||||
private startPromise: Promise<void> | null;
|
||||
|
||||
private systemEnvValues: string[];
|
||||
private projectEnvs: ProjectEnvVariable[];
|
||||
private envValues: Record<string, string>;
|
||||
|
||||
constructor(cwd: string, options: DevServerOptions) {
|
||||
this.cwd = cwd;
|
||||
this.output = options.output;
|
||||
this.envConfigs = { buildEnv: {}, runEnv: {}, allEnv: {} };
|
||||
this.systemEnvValues = options.systemEnvValues || [];
|
||||
this.projectEnvs = options.projectEnvs || [];
|
||||
this.envValues = options.envValues || {};
|
||||
this.files = {};
|
||||
this.originalProjectSettings = options.projectSettings;
|
||||
this.projectSettings = options.projectSettings;
|
||||
@@ -684,16 +681,13 @@ export default class DevServer {
|
||||
|
||||
// If no .env/.build.env is present, use cloud environment variables
|
||||
if (Object.keys(allEnv).length === 0) {
|
||||
const cloudEnv = exposeSystemEnvs(
|
||||
this.projectEnvs || [],
|
||||
this.systemEnvValues || [],
|
||||
this.projectSettings?.autoExposeSystemEnvs,
|
||||
this.address.host
|
||||
);
|
||||
|
||||
allEnv = { ...cloudEnv };
|
||||
runEnv = { ...cloudEnv };
|
||||
buildEnv = { ...cloudEnv };
|
||||
const envValues = { ...this.envValues };
|
||||
if (this.address.host) {
|
||||
envValues['VERCEL_URL'] = this.address.host;
|
||||
}
|
||||
allEnv = { ...envValues };
|
||||
runEnv = { ...envValues };
|
||||
buildEnv = { ...envValues };
|
||||
}
|
||||
|
||||
// legacy NOW_REGION env variable
|
||||
@@ -1454,7 +1448,9 @@ export default class DevServer {
|
||||
}
|
||||
);
|
||||
|
||||
if (middlewareRes.status === 500) {
|
||||
const middlewareBody = await middlewareRes.buffer();
|
||||
|
||||
if (middlewareRes.status === 500 && middlewareBody.byteLength === 0) {
|
||||
await this.sendError(
|
||||
req,
|
||||
res,
|
||||
@@ -1478,6 +1474,9 @@ export default class DevServer {
|
||||
'content-length',
|
||||
'transfer-encoding',
|
||||
]);
|
||||
|
||||
applyOverriddenHeaders(req.headers, middlewareRes.headers);
|
||||
|
||||
for (const [name, value] of middlewareRes.headers) {
|
||||
if (name === 'x-middleware-next') {
|
||||
shouldContinue = value === '1';
|
||||
@@ -1496,7 +1495,6 @@ export default class DevServer {
|
||||
}
|
||||
|
||||
if (!shouldContinue) {
|
||||
const middlewareBody = await middlewareRes.buffer();
|
||||
this.setResponseHeaders(res, requestId);
|
||||
if (middlewareBody.length > 0) {
|
||||
res.setHeader('content-length', middlewareBody.length);
|
||||
@@ -2240,6 +2238,10 @@ export default class DevServer {
|
||||
}
|
||||
);
|
||||
|
||||
// add the node_modules/.bin directory to the PATH
|
||||
const nodeBinPath = await getNodeBinPath({ cwd });
|
||||
env.PATH = `${nodeBinPath}${path.delimiter}${env.PATH}`;
|
||||
|
||||
// This is necesary so that the dev command in the Project
|
||||
// will work cross-platform (especially Windows).
|
||||
let command = devCommand
|
||||
@@ -2254,22 +2256,6 @@ export default class DevServer {
|
||||
})}`
|
||||
);
|
||||
|
||||
const isNpxAvailable = await which('npx')
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (isNpxAvailable) {
|
||||
command = `npx --no-install ${command}`;
|
||||
} else {
|
||||
const isYarnAvailable = await which('yarn')
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (isYarnAvailable) {
|
||||
command = `yarn run --silent ${command}`;
|
||||
}
|
||||
}
|
||||
|
||||
this.output.debug(`Spawning dev command: ${command}`);
|
||||
|
||||
const proxyPort = new RegExp(port.toString(), 'g');
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { VercelConfig } from '@vercel/client';
|
||||
import { HandleValue, Route } from '@vercel/routing-utils';
|
||||
import { Output } from '../output';
|
||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import { BuilderWithPkg } from '../build/import-builders';
|
||||
|
||||
export { VercelConfig };
|
||||
@@ -24,8 +24,7 @@ export { VercelConfig };
|
||||
export interface DevServerOptions {
|
||||
output: Output;
|
||||
projectSettings?: ProjectSettings;
|
||||
systemEnvValues?: string[];
|
||||
projectEnvs?: ProjectEnvVariable[];
|
||||
envValues?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface EnvConfigs {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Org, Project } from '../types';
|
||||
import Client from './client';
|
||||
import setupAndLink from './link/setup-and-link';
|
||||
import param from './output/param';
|
||||
import { getCommandName } from './pkg-name';
|
||||
import { getLinkedProject } from './projects/link';
|
||||
|
||||
type LinkResult = {
|
||||
org: Org;
|
||||
project: Project;
|
||||
};
|
||||
|
||||
export async function ensureLink(
|
||||
commandName: string,
|
||||
client: Client,
|
||||
cwd: string,
|
||||
yes: boolean
|
||||
): Promise<LinkResult | number> {
|
||||
let link = await getLinkedProject(client, cwd);
|
||||
if (link.status === 'not_linked') {
|
||||
link = await setupAndLink(client, cwd, {
|
||||
autoConfirm: yes,
|
||||
successEmoji: 'link',
|
||||
setupMsg: 'Set up',
|
||||
});
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (link.status === 'error') {
|
||||
if (link.reason === 'HEADLESS') {
|
||||
client.output.error(
|
||||
`Command ${getCommandName(
|
||||
commandName
|
||||
)} requires confirmation. Use option ${param('--yes')} to confirm.`
|
||||
);
|
||||
}
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
return { org: link.org, project: link.project };
|
||||
}
|
||||
38
packages/cli/src/util/env/get-env-records.ts
vendored
38
packages/cli/src/util/env/get-env-records.ts
vendored
@@ -2,6 +2,7 @@ import { Output } from '../output';
|
||||
import Client from '../client';
|
||||
import { ProjectEnvVariable, ProjectEnvTarget } from '../../types';
|
||||
import { URLSearchParams } from 'url';
|
||||
import * as path from 'path';
|
||||
|
||||
/** The CLI command that was used that needs the environment variables. */
|
||||
export type EnvRecordsSource =
|
||||
@@ -49,3 +50,40 @@ export default async function getEnvRecords(
|
||||
|
||||
return client.fetch<{ envs: ProjectEnvVariable[] }>(url);
|
||||
}
|
||||
|
||||
interface PullEnvOptions {
|
||||
target?: ProjectEnvTarget | string;
|
||||
gitBranch?: string;
|
||||
}
|
||||
|
||||
export async function pullEnvRecords(
|
||||
output: Output,
|
||||
client: Client,
|
||||
projectId: string,
|
||||
source: EnvRecordsSource,
|
||||
{ target, gitBranch }: PullEnvOptions = {}
|
||||
) {
|
||||
output.debug(
|
||||
`Fetching Environment Variables of project ${projectId} and target ${target}`
|
||||
);
|
||||
const query = new URLSearchParams();
|
||||
|
||||
let url = `/v1/env/pull/${projectId}`;
|
||||
|
||||
if (target) {
|
||||
url = path.join(url, target, gitBranch ?? '');
|
||||
}
|
||||
|
||||
if (source) {
|
||||
query.set('source', source);
|
||||
}
|
||||
|
||||
if (Array.from(query).length > 0) {
|
||||
url += `?${query}`;
|
||||
}
|
||||
|
||||
return client.fetch<{
|
||||
env: Record<string, string>;
|
||||
buildEnv: Record<string, string>;
|
||||
}>(url);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Output } from '../output';
|
||||
import Client from '../client';
|
||||
|
||||
export default async function getSystemEnvValues(
|
||||
output: Output,
|
||||
client: Client,
|
||||
projectId: string
|
||||
) {
|
||||
output.debug(`Fetching System Environment Values of project ${projectId}`);
|
||||
const url = `/v6/projects/${projectId}/system-env-values`;
|
||||
return client.fetch<{ systemEnvValues: string[] }>(url);
|
||||
}
|
||||
2
packages/cli/src/util/env/known-error.ts
vendored
2
packages/cli/src/util/env/known-error.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { isErrnoException } from '../is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const knownErrorsCodes = new Set([
|
||||
'PAYMENT_REQUIRED',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user