Compare commits

...

38 Commits

Author SHA1 Message Date
Steven
eed39913e1 Publish Stable
- @vercel/build-utils@4.2.0
 - vercel@25.2.0
 - @vercel/client@12.0.2
 - @vercel/edge@0.0.1
 - @vercel/frameworks@1.0.2
 - @vercel/go@2.0.2
 - @vercel/next@3.1.0
 - @vercel/node@2.3.0
 - @vercel/python@3.0.2
 - @vercel/redwood@1.0.2
 - @vercel/remix@1.0.2
 - @vercel/routing-utils@1.13.5
 - @vercel/ruby@1.3.10
 - @vercel/static-build@1.0.2
2022-06-28 10:15:33 -04:00
Gal Schlezinger
03e9047bc9 [@vercel/edge] add header helpers (#8036)
* [@vercel/edge] add header helpers

* rename getIp => ipAddress, getGeo => geolocation, as suggested by @kikobeats
2022-06-28 11:26:18 +02:00
Nathan Rajlich
0e35205bf1 [cli][dev][node] Support matchers config for Middleware in vc dev (#8033)
Adds support for `config.matchers` exported property in Middleware during `vc dev`.
2022-06-28 08:34:48 +00:00
Steven
e42fe34c4a [tests] Bump turbo to 1.3.1 (#8011)
https://turborepo.org/blog/turbo-1-3-0
2022-06-28 04:48:35 +00:00
Sean Massa
3ece7ac969 [cli][node] make error handling of edge functions consistent with serverless functions in vc dev (#8007)
When edge functions error, they were showing a basic text response instead of the error template. They were also sending a 502 status code instead of a 500.

This PR makes the error handling of Edge Functions consistent with Serverless Functions when executed through `vc dev`. If we want to update the error templates themselves, we can do that in a separate PR.

**Note:** Production currently treats Edge Function errors differently from Serverless Function errors, but that's a known issue that will be resolved.

---

*I deleted the original outputs (terminal and browser screenshots) because they are out of date and added a lot of content to this page. See my latest comment for updated examples.*
2022-06-28 04:12:21 +00:00
Nathan Rajlich
4f832acf90 [remix] Don't depend on @remix-run/vercel (#8029)
Instead, just add it to the project's `package.json` file before installing the dependencies.

Fixes warning about missing peer dependencies when installing CLI.

<img width="409" alt="CleanShot 2022-05-22 at 09 40 09@2x" src="https://user-images.githubusercontent.com/71256/176084428-79e964b3-8b20-416d-bf3f-c5bd36f4b0ff.png">

Now, a warning is shown in the Deployment build logs, saying that the dep was added, but that the user should commit the change:

<img width="931" alt="Screen Shot 2022-06-27 at 8 15 19 PM" src="https://user-images.githubusercontent.com/71256/176084377-dab5f7d3-4e9f-4bf6-baee-63708b65f218.png">
2022-06-28 03:29:50 +00:00
Matthew Stanciu
918726e01d [cli] Support "http:" scheme in vc bisect (#8023)
`vc bisect` currently prepends `https://` to a passed-in url if it doesn't begin with https—which means that if someone passes in a url that begins with `http://`, it'll turn the url into `https://http://url.com`. This PR fixes this.

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-28 02:15:59 +00:00
JJ Kasper
dc2ddf867b Publish Stable
- @vercel/next@3.0.5
2022-06-27 15:37:09 -05:00
Nathan Rajlich
ee1211416f [cli] Support root-level Middleware file in vc dev (#7973)
Adds initial support for a root-level `middleware.js` / `middleware.ts` file in the `vercel dev` CLI command. This leverages the existing Edge Function invoking logic in `@vercel/node`'s `startDevServer()` function and applies the necessary response / rewrites / mutations to the HTTP request based on the result of the middleware invocation.
2022-06-27 19:56:32 +00:00
JJ Kasper
570fd24e29 Publish Canary
- vercel@25.1.1-canary.11
 - @vercel/edge@0.0.1-canary.0
 - @vercel/next@3.0.5-canary.1
2022-06-27 12:35:09 -05:00
Gal Schlezinger
40681ad0f4 [next] allow to declare edge functions outside of /api/ in Next.js (#7997) 2022-06-27 11:55:39 -05:00
JJ Kasper
f20703b15d [next] Update max size warning to handle initial layer better (#8013)
* Update max size warning to handle initial layer better

* update test
2022-06-27 10:22:48 -05:00
Gal Schlezinger
68eb197112 Add @vercel/edge with helpers for Middleware (#8022) 2022-06-27 17:37:46 +03:00
JJ Kasper
b8b87b96da Publish Canary
- vercel@25.1.1-canary.10
 - @vercel/next@3.0.5-canary.0
2022-06-25 15:04:32 -05:00
JJ Kasper
967c24f1bb [next] Ensure trailing slash is handled while resolving _next/data rewrites (#8015)
* Ensure trailing slash is handled while resolving _next/data rewrites

* update regex
2022-06-25 12:39:30 -05:00
JJ Kasper
609f781234 Publish Stable
- @vercel/next@3.0.4
2022-06-24 23:16:40 -05:00
JJ Kasper
998f6bf6e6 Publish Canary
- vercel@25.1.1-canary.9
 - @vercel/next@3.0.4-canary.3
 - @vercel/node@2.2.1-canary.1
2022-06-23 15:45:31 -05:00
JJ Kasper
7511c2ef85 [next] Fix normalizing for _next/data/index.json route with middleware (#8005)
* Fix normalizing for _next/data/index.json route with middleware

* ensure / -> /index.json denormalizes as well

* add continue/override
2022-06-23 14:57:07 -05:00
Michaël De Boey
71425fac1f [examples] Update remix template (#7917) 2022-06-21 12:20:01 -07:00
Lee Robinson
6973cd5989 [examples] Address follow up from SvelteKit example update (#8002) 2022-06-21 12:10:55 -07:00
Nathan Rajlich
24785ff50a [node] Implement matcher config support for Middleware (#8001)
Allows to specify a string or array of paths/globs for when a root-level
Middleware should be invoked.

```javascript
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}
```
2022-06-21 12:10:13 -07:00
Nathan Rajlich
aa3ad4478c [cli] Only show "Removing .vercel/output" log when directory exists in vc dev (#8004)
Conditionally show the "Removing…" message, instead of always rendering it.
2022-06-21 19:04:24 +00:00
JJ Kasper
f0d73049ca Publish Canary
- vercel@25.1.1-canary.8
 - @vercel/next@3.0.4-canary.2
2022-06-21 10:09:24 -05:00
JJ Kasper
6cef07354a [next] Ensure basePath is matched correctly for _next/data resolving (#7999)
This ensures we correctly match `basePath` for the `_next/data` resolving routes. The tests in the below referenced PR already cover this change so no new fixtures have been added here as they would rely on those changes landing first. 

### Related Issues

x-ref: https://github.com/vercel/next.js/pull/37854

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-20 22:29:01 +00:00
Nathan Rajlich
50af9f5b75 Publish Canary
- vercel@25.1.1-canary.7
 - @vercel/next@3.0.4-canary.1
2022-06-20 00:19:54 -07:00
Seiya Nuta
af76b134d8 [next] Check the size of WASM files in the Edge Functions size validation (#7936)
This is the last missing piece in the size validation of edge functions. Since WASM binaries are not bundled in the user JavaScript file, we also need to count their sizes in the validation.

### Related Issues

### 📋 Checklist

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

#### Tests

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

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-20 04:58:36 +00:00
Nathan Rajlich
c7640005fd [cli] Implement vc build --output parameter (#7995)
Use `--output` parameter to output the Build Output API build artifacts to a different location than the default `.vercel/output` directory.
2022-06-18 23:10:33 +00:00
Nathan Rajlich
3deed977ba [cli] Add requirePath to "builds.json" in vc build (#7990)
Include a `requirePath` property to each "build" in the `builds.json` file which is the absolute path to the Builder entrypoint that was executed.

This gives context as to which Builder was invoked by `vc build` which helps with introspection / debugging.
2022-06-18 15:07:46 +00:00
Nathan Rajlich
b38c360e36 [cli] Include "argv" in builds.json produced by vc build (#7988)
It will be useful for debugging purposes to have access to the arguments that were passed to `vc build`.
2022-06-17 22:18:33 +00:00
JJ Kasper
1595e48414 Publish Canary
- vercel@25.1.1-canary.6
 - @vercel/next@3.0.4-canary.0
2022-06-17 16:32:59 -05:00
JJ Kasper
e6b0ee3e3c [next] Update data regex to be less specific (#7994)
Update data regex to be less specific
2022-06-17 16:32:33 -05:00
JJ Kasper
a247e31688 Publish Stable
- @vercel/next@3.0.3
2022-06-17 14:49:54 -05:00
JJ Kasper
dc02e763a4 Publish Canary
- vercel@25.1.1-canary.5
 - @vercel/next@3.0.3-canary.2
2022-06-17 14:24:39 -05:00
JJ Kasper
8567fc0de6 [next] Optimize _next/data route regex (#7992)
Optimize _next/data route regex
2022-06-17 14:19:37 -05:00
JJ Kasper
4f8f3d373f Publish Canary
- vercel@25.1.1-canary.4
 - @vercel/next@3.0.3-canary.1
 - @vercel/node@2.2.1-canary.0
2022-06-16 18:00:29 -05:00
JJ Kasper
debb85b690 [next] Update error for internal missing page (#7987)
Update error for internal missing page
2022-06-16 16:44:12 -05:00
Thai Pangsakulyanont
bfef989ada [examples] Delete pnpm-lock.yaml from examples/sveltekit because it already contains yarn.lock (#7983)
Delete pnpm-lock.yaml

Co-authored-by: Lee Robinson <lrobinson2011@gmail.com>
2022-06-16 10:24:18 -07:00
JJ Kasper
4e0b6c5eaf [next] Update to skip middleware for on-demand revalidate (#7978)
Update to skip middleware for on-demand revalidate
2022-06-16 11:23:30 -05:00
105 changed files with 2291 additions and 10940 deletions

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
# https://prettier.io/docs/en/ignore.html
# ignore this file with an intentional syntax error
packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js

View File

@@ -0,0 +1,3 @@
module.exports = {
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
};

View File

@@ -1,7 +1,11 @@
node_modules
.cache
.env
.vercel
.output
public/build
api/_build
/build/
/public/build
/api/index.js
/api/index.js.map

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";
export default function handleRequest(
request: Request,
@@ -16,6 +16,6 @@ export default function handleRequest(
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders
headers: responseHeaders,
});
}

View File

@@ -1,3 +1,4 @@
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import {
Link,
Links,
@@ -6,14 +7,13 @@ import {
Outlet,
Scripts,
ScrollRestoration,
useCatch
} from "remix";
import type { LinksFunction } from "remix";
useCatch,
} from "@remix-run/react";
import globalStylesUrl from "~/styles/global.css";
import darkStylesUrl from "~/styles/dark.css";
import globalStylesUrl from "~/styles/global.css";
// https://remix.run/api/app#links
// https://remix.run/api/conventions#links
export let links: LinksFunction = () => {
return [
{ rel: "stylesheet", href: globalStylesUrl },
@@ -25,6 +25,12 @@ export let links: LinksFunction = () => {
];
};
// https://remix.run/api/conventions#meta
export let meta: MetaFunction = () => ({
charset: "utf-8",
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() {
@@ -37,7 +43,7 @@ export default function App() {
);
}
// https://remix.run/docs/en/v1/api/conventions#errorboundary
// https://remix.run/api/conventions#errorboundary
export function ErrorBoundary({ error }: { error: Error }) {
console.error(error);
return (
@@ -57,7 +63,7 @@ export function ErrorBoundary({ error }: { error: Error }) {
);
}
// https://remix.run/docs/en/v1/api/conventions#catchboundary
// https://remix.run/api/conventions#catchboundary
export function CatchBoundary() {
let caught = useCatch();
@@ -103,8 +109,6 @@ function Document({
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{title ? <title>{title}</title> : null}
<Meta />
<Links />
@@ -113,7 +117,7 @@ function Document({
{children}
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === "development" && <LiveReload />}
<LiveReload />
</body>
</html>
);

View File

@@ -1,5 +1,5 @@
import { Outlet } from "remix";
import type { MetaFunction, LinksFunction } from "remix";
import type { MetaFunction, LinksFunction } from "@remix-run/node";
import { Outlet } from "@remix-run/react";
import stylesUrl from "~/styles/demos/about.css";

View File

@@ -1,4 +1,4 @@
import { Link } from "remix";
import { Link } from "@remix-run/react";
export default function AboutIndex() {
return (

View File

@@ -1,4 +1,4 @@
import { Link } from "remix";
import { Link } from "@remix-run/react";
export default function AboutIndex() {
return (

View File

@@ -1,6 +1,7 @@
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";
import type { ActionFunction } from "remix";
import { Form, json, useActionData, redirect } from "remix";
export function meta() {
return { title: "Actions Demo" };

View File

@@ -1,8 +1,8 @@
import { useCatch, Link, json, useLoaderData, Outlet } from "remix";
import type { MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Link, Outlet, useCatch, useLoaderData } from "@remix-run/react";
export function meta() {
return { title: "Boundaries Demo" };
}
export let meta: MetaFunction = () => ({ title: "Boundaries Demo" });
export default function Boundaries() {
return (

View File

@@ -1,5 +1,6 @@
import { useCatch, Link, json, useLoaderData } from "remix";
import type { LoaderFunction, MetaFunction } from "remix";
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.

View File

@@ -1,5 +1,6 @@
import { useCatch, Link, json, useLoaderData, Outlet } from "remix";
import type { LoaderFunction } from "remix";
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 (

View File

@@ -1,5 +1,6 @@
import type { MetaFunction, LoaderFunction } from "remix";
import { useLoaderData, json, Link } from "remix";
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 }>;

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,9 +1,15 @@
/**
* @type {import('@remix-run/dev/config').AppConfig}
* @type {import('@remix-run/dev').AppConfig}
*/
module.exports = {
appDirectory: "app",
browserBuildDirectory: "public/build",
publicPath: "/build/",
serverBuildDirectory: "api/_build"
serverBuildTarget: "vercel",
// When running locally in development mode, we use the built in remix
// server. This does not understand the vercel lambda module format,
// so we default back to the standard build output.
server: process.env.NODE_ENV === "development" ? undefined : "./server.js",
ignoredRouteFiles: ["**/.*"],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "api/index.js",
// publicPath: "/build/",
};

4
examples/remix/server.js Normal file
View File

@@ -0,0 +1,4 @@
import * as build from "@remix-run/dev/server-build";
import { createRequestHandler } from "@remix-run/vercel";
export default createRequestHandler({ build, mode: process.env.NODE_ENV });

View File

@@ -19,7 +19,6 @@
"prettier-plugin-svelte": "^2.5.0",
"svelte": "^3.46.0",
"svelte-check": "^2.2.6",
"svelte-preprocess": "^4.10.6",
"typescript": "~4.6.2"
},
"type": "module",

File diff suppressed because it is too large Load Diff

View File

@@ -7,14 +7,12 @@
let analyticsId = import.meta.env.VERCEL_ANALYTICS_ID;
if (browser && analyticsId) {
page.subscribe(({ url, params }) =>
webVitals({
path: url.pathname,
params,
analyticsId
})
);
$: if (browser && analyticsId) {
webVitals({
path: $page.url.pathname,
params: $page.params,
analyticsId
})
}
</script>

View File

@@ -1,19 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: preprocess({
replace: [
['import.meta.env.VERCEL_ANALYTICS_ID', JSON.stringify(process.env.VERCEL_ANALYTICS_ID)]
]
}),
kit: {
adapter: adapter(),
// Override http methods in the Todo forms
methodOverride: {
allowed: ['PATCH', 'DELETE']
},
vite: {
define: {
'import.meta.env.VERCEL_ANALYTICS_ID': JSON.stringify(process.env.VERCEL_ANALYTICS_ID)
}
}
}
};

View File

@@ -31,7 +31,7 @@
"prettier": "2.6.2",
"ts-eager": "2.0.2",
"ts-jest": "28.0.5",
"turbo": "1.2.14"
"turbo": "1.3.1"
},
"scripts": {
"lerna": "lerna",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "4.1.1-canary.1",
"version": "4.2.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -31,7 +31,7 @@
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "2.4.2",
"@vercel/frameworks": "1.0.2-canary.0",
"@vercel/frameworks": "1.0.2",
"@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "25.1.1-canary.3",
"version": "25.2.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -42,15 +42,15 @@
"node": ">= 14"
},
"dependencies": {
"@vercel/build-utils": "4.1.1-canary.1",
"@vercel/go": "2.0.2-canary.1",
"@vercel/next": "3.0.3-canary.0",
"@vercel/node": "2.2.0",
"@vercel/python": "3.0.2-canary.1",
"@vercel/redwood": "1.0.2-canary.1",
"@vercel/remix": "1.0.2-canary.1",
"@vercel/ruby": "1.3.10-canary.1",
"@vercel/static-build": "1.0.2-canary.1",
"@vercel/build-utils": "4.2.0",
"@vercel/go": "2.0.2",
"@vercel/next": "3.1.0",
"@vercel/node": "2.3.0",
"@vercel/python": "3.0.2",
"@vercel/redwood": "1.0.2",
"@vercel/remix": "1.0.2",
"@vercel/ruby": "1.3.10",
"@vercel/static-build": "1.0.2",
"update-notifier": "5.1.0"
},
"devDependencies": {
@@ -95,8 +95,8 @@
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.0.2-canary.1",
"@vercel/frameworks": "1.0.2-canary.0",
"@vercel/client": "12.0.2",
"@vercel/frameworks": "1.0.2",
"@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2",

View File

@@ -16,6 +16,7 @@ import Client from '../../util/client';
import { getPkgName } from '../../util/pkg-name';
import { Output } from '../../util/output';
import { Deployment, PaginationOptions } from '../../types';
import { normalizeURL } from '../../util/bisect/normalize-url';
interface DeploymentV6
extends Pick<
@@ -97,9 +98,7 @@ export default async function main(client: Client): Promise<number> {
run = resolve(run);
}
if (!bad.startsWith('https://')) {
bad = `https://${bad}`;
}
bad = normalizeURL(bad);
let parsed = parse(bad);
if (!parsed.hostname) {
output.error('Invalid input: no hostname provided');
@@ -120,9 +119,7 @@ export default async function main(client: Client): Promise<number> {
const badDeploymentPromise = getDeployment(client, bad).catch(err => err);
if (!good.startsWith('https://')) {
good = `https://${good}`;
}
good = normalizeURL(good);
parsed = parse(good);
if (!parsed.hostname) {
output.error('Invalid input: no hostname provided');

View File

@@ -1,7 +1,7 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import dotenv from 'dotenv';
import { join, normalize, relative } from 'path';
import { join, normalize, relative, resolve } from 'path';
import {
detectBuilders,
normalizePath,
@@ -65,6 +65,7 @@ const help = () => {
'DIR'
)} Path to the global ${'`.vercel`'} directory
--cwd [path] The current working directory
--output [path] Directory where built assets should be written to
--prod Build a production deployment
-d, --debug Debug mode [off]
-y, --yes Skip the confirmation prompt
@@ -101,6 +102,7 @@ export default async function main(client: Client): Promise<number> {
// Parse CLI args
const argv = getArgs(client.argv.slice(2), {
'--cwd': String,
'--output': String,
'--prod': Boolean,
'--yes': Boolean,
});
@@ -280,7 +282,9 @@ export default async function main(client: Client): Promise<number> {
}
// Delete output directory from potential previous build
const outputDir = join(cwd, OUTPUT_DIR);
const outputDir = argv['--output']
? resolve(argv['--output'])
: join(cwd, OUTPUT_DIR);
await fs.remove(outputDir);
const buildStamp = stamp();
@@ -297,6 +301,7 @@ export default async function main(client: Client): Promise<number> {
{
'//': 'This file was generated by the `vercel build` command. It is not part of the Build Output API.',
target,
argv: process.argv,
builds: builds.map(build => {
const builderWithPkg = buildersWithPkgs.get(build.use);
if (!builderWithPkg) {
@@ -305,6 +310,7 @@ export default async function main(client: Client): Promise<number> {
const { builder, pkg: builderPkg } = builderWithPkg;
return {
require: builderPkg.name,
requirePath: builderWithPkg.path,
apiVersion: builder.version,
...build,
};
@@ -463,11 +469,12 @@ export default async function main(client: Client): Promise<number> {
};
await fs.writeJSON(join(outputDir, 'config.json'), config, { spaces: 2 });
const relOutputDir = relative(cwd, outputDir);
output.print(
`${prependEmoji(
`Build Completed in ${chalk.bold(OUTPUT_DIR)} ${chalk.gray(
buildStamp()
)}`,
`Build Completed in ${chalk.bold(
relOutputDir.startsWith('..') ? outputDir : relOutputDir
)} ${chalk.gray(buildStamp())}`,
emoji('success')
)}\n`
);

View File

@@ -110,9 +110,11 @@ export default async function dev(
// v3 Build Output because it will incorrectly be detected by
// @vercel/static-build in BuildOutputV3.getBuildOutputDirectory()
if (!devCommand) {
output.log(`Removing ${OUTPUT_DIR}`);
const outputDir = join(cwd, OUTPUT_DIR);
await fs.remove(outputDir);
if (await fs.pathExists(outputDir)) {
output.log(`Removing ${OUTPUT_DIR}`);
await fs.remove(outputDir);
}
}
const devServer = new DevServer(cwd, {

View File

@@ -0,0 +1,7 @@
function hasScheme(url: string): Boolean {
return url.startsWith('http://') || url.startsWith('https://');
}
export function normalizeURL(url: string): string {
return hasScheme(url) ? url : `https://${url}`;
}

View File

@@ -425,10 +425,6 @@ export async function getBuildMatches(
src = extensionless;
}
// We need to escape brackets since `glob` will
// try to find a group otherwise
src = src.replace(/(\[|\])/g, '[$1]');
const files = fileList
.filter(name => name === src || minimatch(name, src, { dot: true }))
.map(name => join(cwd, name));

View File

@@ -0,0 +1,18 @@
import { Headers } from 'node-fetch';
import { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http';
export function nodeHeadersToFetchHeaders(
nodeHeaders: IncomingHttpHeaders | OutgoingHttpHeaders
): Headers {
const headers = new Headers();
for (const [name, value] of Object.entries(nodeHeaders)) {
if (Array.isArray(value)) {
for (const val of value) {
headers.append(name, val);
}
} else if (typeof value !== 'undefined') {
headers.set(name, String(value));
}
}
return headers;
}

View File

@@ -1,12 +1,13 @@
import ms from 'ms';
import url, { URL } from 'url';
import http from 'http';
import fs from 'fs-extra';
import chalk from 'chalk';
import fetch from 'node-fetch';
import plural from 'pluralize';
import rawBody from 'raw-body';
import listen from 'async-listen';
import minimatch from 'minimatch';
import ms from 'ms';
import httpProxy from 'http-proxy';
import { randomBytes } from 'crypto';
import serveHandler from 'serve-handler';
@@ -16,11 +17,11 @@ import path, { isAbsolute, basename, dirname, extname, join } from 'path';
import once from '@tootallnate/once';
import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port';
import { ChildProcess } from 'child_process';
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';
import { getVercelIgnore, fileNameSymbol } from '@vercel/client';
import {
@@ -90,6 +91,7 @@ import {
import { ProjectEnvVariable, ProjectSettings } from '../../types';
import exposeSystemEnvs from './expose-system-envs';
import { treeKill } from '../tree-kill';
import { nodeHeadersToFetchHeaders } from './headers';
const frontendRuntimeSet = new Set(
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
@@ -593,7 +595,7 @@ export default class DevServer {
await this.exit();
}
if (warnings && warnings.length > 0) {
if (warnings?.length > 0) {
warnings.forEach(warning =>
this.output.warn(warning.message, null, warning.link, warning.action)
);
@@ -1106,6 +1108,7 @@ export default class DevServer {
view = errorTemplate({
http_status_code: statusCode,
http_status_description,
error_code,
request_id: requestId,
});
}
@@ -1337,32 +1340,6 @@ export default class DevServer {
return false;
};
/*
runDevMiddleware = async (
req: http.IncomingMessage,
res: http.ServerResponse
) => {
const { devMiddlewarePlugins } = await loadCliPlugins(
this.cwd,
this.output
);
try {
for (let plugin of devMiddlewarePlugins) {
const result = await plugin.plugin.runDevMiddleware(req, res, this.cwd);
if (result.finished) {
return result;
}
}
return { finished: false };
} catch (e) {
return {
finished: true,
error: e,
};
}
};
*/
/**
* Serve project directory as a v2 deployment.
*/
@@ -1429,13 +1406,143 @@ export default class DevServer {
let statusCode: number | undefined;
let prevUrl = req.url;
let prevHeaders: HttpHeadersConfig = {};
let middlewarePid: number | undefined;
/*
const middlewareResult = await this.runDevMiddleware(req, res);
// Run the middleware file, if present, and apply any
// mutations to the incoming request based on the
// result of the middleware invocation.
const middleware = [...this.buildMatches.values()].find(
m => m.config?.middleware === true
);
if (middleware) {
let startMiddlewareResult: StartDevServerResult | undefined;
// TODO: can we add some caching to prevent (re-)starting
// the middleware server for every HTTP request?
const { envConfigs, files, devCacheDir, cwd: workPath } = this;
try {
startMiddlewareResult =
await middleware.builderWithPkg.builder.startDevServer?.({
files,
entrypoint: middleware.entrypoint,
workPath,
repoRootPath: this.cwd,
config: middleware.config || {},
meta: {
isDev: true,
devCacheDir,
requestUrl: req.url,
env: { ...envConfigs.runEnv },
buildEnv: { ...envConfigs.buildEnv },
},
});
if (middlewareResult) {
if (middlewareResult.error) {
this.sendError(
if (startMiddlewareResult) {
const { port, pid } = startMiddlewareResult;
middlewarePid = pid;
this.devServerPids.add(pid);
const middlewareReqHeaders = nodeHeadersToFetchHeaders(req.headers);
// Add the Vercel platform proxy request headers
const proxyHeaders = this.getProxyHeaders(req, requestId, true);
for (const [name, value] of nodeHeadersToFetchHeaders(proxyHeaders)) {
middlewareReqHeaders.set(name, value);
}
const middlewareRes = await fetch(
`http://127.0.0.1:${port}${parsed.path}`,
{
headers: middlewareReqHeaders,
method: req.method,
redirect: 'manual',
}
);
if (middlewareRes.status === 500) {
await this.sendError(
req,
res,
requestId,
'EDGE_FUNCTION_INVOCATION_FAILED',
500
);
return;
}
// Apply status code from middleware invocation,
// for i.e. redirects or a custom 404 page
res.statusCode = middlewareRes.status;
let rewritePath = '';
let contentType = '';
let shouldContinue = false;
const skipMiddlewareHeaders = new Set([
'date',
'connection',
'content-length',
'transfer-encoding',
]);
for (const [name, value] of middlewareRes.headers) {
if (name === 'x-middleware-next') {
shouldContinue = value === '1';
} else if (name === 'x-middleware-rewrite') {
rewritePath = value;
shouldContinue = true;
} else if (name === 'content-type') {
contentType = value;
} else if (!skipMiddlewareHeaders.has(name)) {
// Any other kind of response header should be included
// on both the incoming HTTP request (for when proxying
// to another function) and the outgoing HTTP response.
res.setHeader(name, value);
req.headers[name] = value;
}
}
if (!shouldContinue) {
const middlewareBody = await middlewareRes.buffer();
this.setResponseHeaders(res, requestId);
if (middlewareBody.length > 0) {
res.setHeader('content-length', middlewareBody.length);
if (contentType) {
res.setHeader('content-type', contentType);
}
res.end(middlewareBody);
} else {
res.end();
}
return;
}
if (rewritePath) {
// TODO: add validation?
debug(`Detected rewrite path from middleware: "${rewritePath}"`);
prevUrl = rewritePath;
// Retain orginal pathname, but override query parameters from the rewrite
const beforeRewriteUrl = req.url || '/';
const rewriteUrlParsed = url.parse(beforeRewriteUrl, true);
delete rewriteUrlParsed.search;
rewriteUrlParsed.query = url.parse(rewritePath, true).query;
req.url = url.format(rewriteUrlParsed);
debug(
`Rewrote incoming HTTP URL from "${beforeRewriteUrl}" to "${req.url}"`
);
}
}
} catch (err) {
// `startDevServer()` threw an error. Most likely this means the dev
// server process exited before sending the port information message
// (missing dependency at runtime, for example).
if (err.code === 'ENOENT') {
err.message = `Command not found: ${chalk.cyan(
err.path,
...err.spawnargs
)}\nPlease ensure that ${cmd(err.path)} is properly installed`;
err.link = 'https://vercel.link/command-not-found';
}
await this.sendError(
req,
res,
requestId,
@@ -1443,24 +1550,12 @@ export default class DevServer {
500
);
return;
}
if (middlewareResult.finished) {
return;
}
if (middlewareResult.pathname) {
const origUrl = url.parse(req.url || '/', true);
origUrl.pathname = middlewareResult.pathname;
prevUrl = url.format(origUrl);
}
if (middlewareResult.query && prevUrl) {
const origUrl = url.parse(req.url || '/', true);
delete origUrl.search;
Object.assign(origUrl.query, middlewareResult.query);
prevUrl = url.format(origUrl);
} finally {
if (middlewarePid) {
this.killBuilderDevServer(middlewarePid);
}
}
}
*/
for (const phase of phases) {
statusCode = undefined;
@@ -1740,7 +1835,10 @@ export default class DevServer {
isDev: true,
requestPath,
devCacheDir,
env: { ...envConfigs.runEnv },
env: {
...envConfigs.runEnv,
VERCEL_BUILDER_DEBUG: this.output.debugEnabled ? '1' : undefined,
},
buildEnv: { ...envConfigs.buildEnv },
},
});
@@ -2185,13 +2283,7 @@ function proxyPass(
`Failed to complete request to ${req.url}: ${error}`
);
if (!res.headersSent) {
devServer.sendError(
req,
res,
requestId,
'NO_RESPONSE_FROM_FUNCTION',
502
);
devServer.sendError(req, res, requestId, 'FUNCTION_INVOCATION_FAILED');
}
}
);
@@ -2269,11 +2361,12 @@ async function findBuildMatch(
if (!isIndex(match.src)) {
return match;
} else {
// if isIndex === true and ends in .html, we're done. Otherwise, keep searching
bestIndexMatch = match;
// If isIndex === true and ends in `.html`, we're done.
// Otherwise, keep searching.
if (extname(match.src) === '.html') {
return bestIndexMatch;
return match;
}
bestIndexMatch = match;
}
}
}
@@ -2295,6 +2388,13 @@ async function shouldServe(
config,
builderWithPkg: { builder },
} = match;
// "middleware" file is not served as a regular asset,
// instead it gets invoked as part of the routing logic.
if (config?.middleware === true) {
return false;
}
const cleanSrc = src.endsWith('.html') ? src.slice(0, -5) : src;
const trimmedPath = requestPath.endsWith('/')
? requestPath.slice(0, -1)

View File

@@ -0,0 +1,7 @@
export const config = {
runtime: 'invalid-runtime-value',
};
export default async function edge(request, event) {
throw new Error('intentional runtime error');
}

View File

@@ -0,0 +1,8 @@
export const config = {
runtime: 'experimental-edge',
};
export async function notTheDefaultExport(request, event) {
// this will never be run
return new Response('some response body');
}

View File

@@ -0,0 +1,7 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
throw new Error('intentional runtime error');
}

View File

@@ -0,0 +1,10 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
// this should never be executed
return new Response('some response body');
}
throw new Error('intentional startup error');

View File

@@ -0,0 +1,9 @@
export const config = {
runtime: 'experimental-edge'
}
export default async function edge(request: Request, event: Event) {
return new Response('some response body');
// intentional missing closing bracket to produce syntax error
// }

View File

@@ -0,0 +1,9 @@
import unknownModule from 'unknown-module-893427589372458934795843';
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
return new Response(unknownModule('some response body'));
}

View File

@@ -0,0 +1,9 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request, event) {
return new Response('responding with intentional 500 from user code', {
status: 500,
});
}

View File

@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
decamelize@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-6.0.0.tgz#8cad4d916fde5c41a264a43d0ecc56fe3d31749e"
integrity sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==

View File

@@ -0,0 +1 @@
export default () => new Response(null, { status: 500 });

View File

@@ -0,0 +1 @@
throw new Error('Middleware init error');

View File

@@ -0,0 +1,3 @@
export default () => {
throw new Error('Middleware handler error');
};

View File

@@ -0,0 +1,13 @@
// Supports both a single string value or an array of matchers
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
};
export default function middleware(request, _event) {
const response = new Response('middleware response');
// Set custom header
response.headers.set('x-modified-edge', 'true');
return response;
}

View File

@@ -0,0 +1,9 @@
export default req => {
const url = new URL(req.url);
return new Response(null, {
status: 302,
headers: {
location: `https://vercel.com${url.pathname}${url.search}`,
},
});
};

View File

@@ -0,0 +1 @@
export default () => new Response('hi from middleware');

View File

@@ -0,0 +1,5 @@
export default (req, res) => {
res.json({
url: req.url,
});
};

View File

@@ -0,0 +1,6 @@
export default () =>
new Response(null, {
headers: {
'x-middleware-rewrite': '/api/fn?from-middleware=true',
},
});

View File

@@ -0,0 +1 @@
<h1>Another</h1>

View File

@@ -0,0 +1 @@
<h1>Index</h1>

View File

@@ -0,0 +1,19 @@
export default req => {
const url = new URL(req.url);
if (url.pathname === '/') {
// Pass-through "index.html" page
return new Response(null, {
headers: {
'x-middleware-next': '1',
},
});
}
// Everything else goes to "another.html"
return new Response(null, {
headers: {
'x-middleware-rewrite': '/another.html',
},
});
};

View File

@@ -23,7 +23,7 @@ test('[vercel dev] should support edge functions', async () => {
const body = { hello: 'world' };
let res = await fetch(`http://localhost:${port}/api/edge-function`, {
let res = await fetch(`http://localhost:${port}/api/edge-success`, {
method: 'POST',
headers: {
'content-type': 'application/json',
@@ -36,7 +36,7 @@ test('[vercel dev] should support edge functions', async () => {
// are set up; so, we test that they are all passed through properly
expect(await res.json()).toMatchObject({
headerContentType: 'application/json',
url: `http://localhost:${port}/api/edge-function`,
url: `http://localhost:${port}/api/edge-success`,
method: 'POST',
body: '{"hello":"world"}',
decamelized: 'some_camel_case_thing',
@@ -48,6 +48,237 @@ test('[vercel dev] should support edge functions', async () => {
}
});
test(
'[vercel dev] edge functions respond properly the same as production',
testFixtureStdio('edge-function', async (testPath: any) => {
await testPath(500, '/api/edge-500-response');
await testPath(200, '/api/edge-success');
})
);
test('[vercel dev] should support edge functions returning intentional 500 responses', async () => {
const dir = fixture('edge-function');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const body = { hello: 'world' };
let res = await fetch(`http://localhost:${port}/api/edge-500-response`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
validateResponseHeaders(res);
expect(await res.status).toBe(500);
expect(await res.text()).toBe(
'responding with intentional 500 from user code'
);
} finally {
await dev.kill('SIGTERM');
}
});
test('[vercel dev] should handle runtime errors thrown in edge functions', async () => {
const dir = fixture('edge-function-error');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(`http://localhost:${port}/api/edge-error-runtime`, {
method: 'GET',
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
});
validateResponseHeaders(res);
const { stdout, stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stdout).toMatch(/Unhandled rejection: intentional runtime error/g);
expect(stderr).toMatch(
/Failed to complete request to \/api\/edge-error-runtime: Error: socket hang up/g
);
} finally {
await dev.kill('SIGTERM');
}
});
test('[vercel dev] should handle config errors thrown in edge functions', async () => {
const dir = fixture('edge-function-error');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(`http://localhost:${port}/api/edge-error-config`, {
method: 'GET',
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
});
validateResponseHeaders(res);
const { stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stderr).toMatch(
/Invalid function runtime "invalid-runtime-value" for "api\/edge-error-config.js". Valid runtimes are: \["experimental-edge"\]/g
);
expect(stderr).toMatch(
/Failed to complete request to \/api\/edge-error-config: Error: socket hang up/g
);
} finally {
await dev.kill('SIGTERM');
}
});
test('[vercel dev] should handle startup errors thrown in edge functions', async () => {
const dir = fixture('edge-function-error');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(`http://localhost:${port}/api/edge-error-startup`, {
method: 'GET',
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
});
validateResponseHeaders(res);
const { stdout, stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stdout).toMatch(
/Failed to instantiate edge runtime: intentional startup error/g
);
expect(stderr).toMatch(
/Failed to complete request to \/api\/edge-error-startup: Error: socket hang up/g
);
} finally {
await dev.kill('SIGTERM');
}
});
test('[vercel dev] should handle syntax errors thrown in edge functions', async () => {
const dir = fixture('edge-function-error');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(`http://localhost:${port}/api/edge-error-syntax`, {
method: 'GET',
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
});
validateResponseHeaders(res);
const { stdout, stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stdout).toMatch(
/Failed to instantiate edge runtime: Module parse failed: Unexpected token/g
);
expect(stderr).toMatch(
/Failed to complete request to \/api\/edge-error-syntax: Error: socket hang up/g
);
} finally {
await dev.kill('SIGTERM');
}
});
test('[vercel dev] should handle import errors thrown in edge functions', async () => {
const dir = fixture('edge-function-error');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(
`http://localhost:${port}/api/edge-error-unknown-import`,
{
method: 'GET',
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
}
);
validateResponseHeaders(res);
const { stdout, stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stdout).toMatch(
/Failed to instantiate edge runtime: Code generation from strings disallowed for this context/g
);
expect(stderr).toMatch(
/Failed to complete request to \/api\/edge-error-unknown-import: Error: socket hang up/g
);
} finally {
await dev.kill('SIGTERM');
}
});
test('[vercel dev] should handle import errors thrown in edge functions', async () => {
const dir = fixture('edge-function-error');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(
`http://localhost:${port}/api/edge-error-no-handler`,
{
method: 'GET',
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
}
);
validateResponseHeaders(res);
const { stdout, stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stdout).toMatch(
/No default export was found. Add a default export to handle requests./g
);
expect(stderr).toMatch(
/Failed to complete request to \/api\/edge-error-no-handler: Error: socket hang up/g
);
} finally {
await dev.kill('SIGTERM');
}
});
test('[vercel dev] should support request body', async () => {
const dir = fixture('node-request-body');
const { dev, port, readyResolver } = await testFixture(dir);

View File

@@ -433,3 +433,93 @@ test(
await testPath(404, '/i-do-not-exist');
})
);
test(
'[vercel dev] Middleware that returns a 200 response',
testFixtureStdio('middleware-response', async (testPath: any) => {
await testPath(200, '/', 'hi from middleware');
await testPath(200, '/another', 'hi from middleware');
})
);
test(
'[vercel dev] Middleware that does basic rewrite',
testFixtureStdio('middleware-rewrite', async (testPath: any) => {
await testPath(200, '/', '<h1>Index</h1>');
await testPath(200, '/index', '<h1>Another</h1>');
await testPath(200, '/another', '<h1>Another</h1>');
await testPath(200, '/another.html', '<h1>Another</h1>');
await testPath(200, '/foo', '<h1>Another</h1>');
})
);
test(
'[vercel dev] Middleware that rewrites with custom query params',
testFixtureStdio('middleware-rewrite-query', async (testPath: any) => {
await testPath(200, '/?foo=bar', '{"url":"/?from-middleware=true"}');
await testPath(
200,
'/another?foo=bar',
'{"url":"/another?from-middleware=true"}'
);
await testPath(
200,
'/api/fn?foo=bar',
'{"url":"/api/fn?from-middleware=true"}'
);
})
);
test(
'[vercel dev] Middleware that redirects',
testFixtureStdio('middleware-redirect', async (testPath: any) => {
await testPath(302, '/', null, {
location: 'https://vercel.com/',
});
await testPath(302, '/home', null, {
location: 'https://vercel.com/home',
});
await testPath(302, '/?foo=bar', null, {
location: 'https://vercel.com/?foo=bar',
});
})
);
test(
'[vercel dev] Middleware with error in function handler',
testFixtureStdio('middleware-error-in-handler', async (testPath: any) => {
await testPath(500, '/', /EDGE_FUNCTION_INVOCATION_FAILED/);
})
);
test(
'[vercel dev] Middleware with error at init',
testFixtureStdio('middleware-error-at-init', async (testPath: any) => {
await testPath(500, '/', /EDGE_FUNCTION_INVOCATION_FAILED/);
})
);
test(
'[vercel dev] Middleware with an explicit 500 response',
testFixtureStdio('middleware-500-response', async (testPath: any) => {
await testPath(500, '/', /EDGE_FUNCTION_INVOCATION_FAILED/);
})
);
test(
'[vercel dev] Middleware with `matchers` config',
testFixtureStdio(
'middleware-matchers',
async (testPath: any) => {
// TODO: remove once latest `@vercel/node` is shipped to stable with `matchers` support (fails because `directoryListing`)
//await testPath(404, '/');
await testPath(404, '/another');
await testPath(200, '/about/page', 'middleware response');
await testPath(200, '/dashboard/home', 'middleware response');
},
{
// TODO: remove once latest `@vercel/node` is shipped to stable with `matchers` support
skipDeploy: true,
}
)
);

View File

@@ -257,6 +257,10 @@ async function testFixture(directory, opts = {}, args = []) {
dev.kill = async (...args) => {
dev._kill(...args);
await exitResolver;
return {
stdout,
stderr,
};
};
return {

View File

@@ -0,0 +1,7 @@
{
"orgId": ".",
"projectId": ".",
"settings": {
"framework": null
}
}

View File

@@ -0,0 +1 @@
<h1>Vercel</h1>

View File

@@ -0,0 +1,5 @@
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
};
export default () => new Response('middleware');

View File

@@ -1 +1 @@
export default req => new Response('middleware');
export default () => new Response('middleware');

View File

@@ -1,6 +1,7 @@
import ms from 'ms';
import fs from 'fs-extra';
import { join } from 'path';
import { getWriteableDirectory } from '@vercel/build-utils';
import build from '../../../src/commands/build';
import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
@@ -401,7 +402,7 @@ describe('build', () => {
expect(config).toMatchObject({
version: 3,
routes: [
{ src: '/.*', middlewarePath: 'middleware', continue: true },
{ src: '^/.*$', middlewarePath: 'middleware', continue: true },
{ handle: 'filesystem' },
{ src: '^/api(/.*)?$', status: 404 },
{ handle: 'error' },
@@ -421,4 +422,101 @@ describe('build', () => {
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
it('should build root-level `middleware.js` with "matcher" config', async () => {
const cwd = fixture('middleware-with-matcher');
const output = join(cwd, '.vercel/output');
try {
process.chdir(cwd);
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/static" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'middleware.js',
config: {
zeroConfig: true,
middleware: true,
},
},
{
require: '@vercel/static',
apiVersion: 2,
use: '@vercel/static',
src: '!{api/**,package.json,middleware.[jt]s}',
config: {
zeroConfig: true,
},
},
],
});
// `config.json` includes the "middlewarePath" route
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toMatchObject({
version: 3,
routes: [
{
src: '^\\/about(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?[\\/#\\?]?$|^\\/dashboard(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?[\\/#\\?]?$',
middlewarePath: 'middleware',
continue: true,
},
{ handle: 'filesystem' },
{ src: '^/api(/.*)?$', status: 404 },
{ handle: 'error' },
{ status: 404, src: '^(?!/api).*$', dest: '/404.html' },
],
});
// "static" directory contains `index.html`, but *not* `middleware.js`
const staticFiles = await fs.readdir(join(output, 'static'));
expect(staticFiles.sort()).toEqual(['index.html']);
// "functions" directory contains `middleware.func`
const functions = await fs.readdir(join(output, 'functions'));
expect(functions.sort()).toEqual(['middleware.func']);
} finally {
process.chdir(originalCwd);
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
it('should support `--output` parameter', async () => {
const cwd = fixture('static');
const output = await getWriteableDirectory();
try {
process.chdir(cwd);
client.setArgv('build', '--output', output);
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/static" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/static',
apiVersion: 2,
src: '**',
use: '@vercel/static',
},
],
});
// "static" directory contains static files
const files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.html']);
} finally {
process.chdir(originalCwd);
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
});

View File

@@ -0,0 +1,16 @@
import { normalizeURL } from '../../../../src/util/bisect/normalize-url';
describe('normalize-url', () => {
it('should add https to url without scheme', () => {
const normalizedUrl = normalizeURL('vercel.com');
expect(normalizedUrl).toEqual('https://vercel.com');
});
it('should not add anything to a url that starts with https', () => {
const normalizedUrl = normalizeURL('https://vercel.com');
expect(normalizedUrl).toEqual('https://vercel.com');
});
it('should not add anything to a url that starts with http', () => {
const normalizedUrl = normalizeURL('http://vercel.com');
expect(normalizedUrl).toEqual('http://vercel.com');
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "12.0.2-canary.1",
"version": "12.0.2",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -42,7 +42,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "4.1.1-canary.1",
"@vercel/build-utils": "4.2.0",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

3
packages/edge/README.md Normal file
View File

@@ -0,0 +1,3 @@
# `@vercel/edge`
A set of utilities to help you deploy your app on the Edge using Vercel.

View File

@@ -0,0 +1,30 @@
{
"name": "@vercel/edge",
"version": "0.0.1",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --dts --format esm,cjs",
"test-unit": "jest",
"prepublishOnly": "yarn build"
},
"devDependencies": {
"@edge-runtime/jest-environment": "1.1.0-beta.7",
"@types/jest": "27.4.1",
"ts-node": "8.9.1",
"tsup": "6.1.2",
"typescript": "4.7.4"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"globals": {
"ts-jest": {
"diagnostics": true,
"isolatedModules": true
}
}
}
}

View File

@@ -0,0 +1,82 @@
/**
* City of the original client IP calculated by Vercel Proxy.
*/
export const CITY_HEADER_NAME = 'x-vercel-ip-city';
/**
* Country of the original client IP calculated by Vercel Proxy.
*/
export const COUNTRY_HEADER_NAME = 'x-vercel-ip-country';
/**
* Ip from Vercel Proxy. Do not confuse it with the client Ip.
*/
export const IP_HEADER_NAME = 'x-real-ip';
/**
* Latitude of the original client IP calculated by Vercel Proxy.
*/
export const LATITUDE_HEADER_NAME = 'x-vercel-ip-latitude';
/**
* Longitude of the original client IP calculated by Vercel Proxy.
*/
export const LONGITUDE_HEADER_NAME = 'x-vercel-ip-longitude';
/**
* Region of the original client IP calculated by Vercel Proxy.
*/
export const REGION_HEADER_NAME = 'x-vercel-ip-country-region';
/**
* We define a new type so this function can be reused with
* the global `Request`, `node-fetch` and other types.
*/
interface Request {
headers: {
get(name: string): string | null;
};
}
/**
* The location information of a given request
*/
export interface Geo {
/** The city that the request originated from */
city?: string;
/** The country that the request originated from */
country?: string;
/** The Vercel Edge Network region that received the request */
region?: string;
/** The latitude of the client */
latitude?: string;
/** The longitude of the client */
longitude?: string;
}
function getHeader(request: Request, key: string): string | undefined {
return request.headers.get(key) ?? undefined;
}
/**
* Returns the IP address of the request from the headers.
*
* @see {@link IP_HEADER_NAME}
*/
export function ipAddress(request: Request): string | undefined {
return getHeader(request, IP_HEADER_NAME);
}
/**
* Returns the location information from for the incoming request
*
* @see {@link CITY_HEADER_NAME}
* @see {@link COUNTRY_HEADER_NAME}
* @see {@link REGION_HEADER_NAME}
* @see {@link LATITUDE_HEADER_NAME}
* @see {@link LONGITUDE_HEADER_NAME}
*/
export function geolocation(request: Request): Geo {
return {
city: getHeader(request, CITY_HEADER_NAME),
country: getHeader(request, COUNTRY_HEADER_NAME),
region: getHeader(request, REGION_HEADER_NAME),
latitude: getHeader(request, LATITUDE_HEADER_NAME),
longitude: getHeader(request, LONGITUDE_HEADER_NAME),
};
}

View File

@@ -0,0 +1,5 @@
export type { ExtraResponseInit } from './middleware-helpers';
export * from './middleware-helpers';
export type { Geo } from './edge-headers';
export * from './edge-headers';

View File

@@ -0,0 +1,34 @@
export type ExtraResponseInit = Omit<ResponseInit, 'headers'> & {
/**
* These headers will be sent to the user response
* along with the response headers from the origin
*/
headers?: HeadersInit;
};
/**
* Rewrite the request into a different URL.
*/
export function rewrite(
destination: string | URL,
init?: ExtraResponseInit
): Response {
const headers = new Headers(init?.headers ?? {});
headers.set('x-middleware-rewrite', String(destination));
return new Response(null, {
...init,
headers,
});
}
/**
* This tells the Middleware to continue with the request.
*/
export function next(init?: ExtraResponseInit): Response {
const headers = new Headers(init?.headers ?? {});
headers.set('x-middleware-next', '1');
return new Response(null, {
...init,
headers,
});
}

50
packages/edge/test/edge-headers.test.ts vendored Normal file
View File

@@ -0,0 +1,50 @@
/**
* @jest-environment @edge-runtime/jest-environment
*/
import {
CITY_HEADER_NAME,
COUNTRY_HEADER_NAME,
Geo,
geolocation,
ipAddress,
IP_HEADER_NAME,
LATITUDE_HEADER_NAME,
LONGITUDE_HEADER_NAME,
REGION_HEADER_NAME,
} from '../src';
test('`ipAddress` returns the value from the header', () => {
const req = new Request('https://example.vercel.sh', {
headers: {
[IP_HEADER_NAME]: '127.0.0.1',
},
});
expect(ipAddress(req)).toBe('127.0.0.1');
});
describe('`geolocation`', () => {
test('returns an empty object if headers are not found', () => {
const req = new Request('https://example.vercel.sh');
expect(geolocation(req)).toEqual({});
});
test('reads values from headers', () => {
const req = new Request('https://example.vercel.sh', {
headers: {
[CITY_HEADER_NAME]: 'Tel Aviv',
[COUNTRY_HEADER_NAME]: 'Israel',
[LATITUDE_HEADER_NAME]: '32.109333',
[LONGITUDE_HEADER_NAME]: '34.855499',
[REGION_HEADER_NAME]: 'fra1',
},
});
expect(geolocation(req)).toEqual<Geo>({
city: 'Tel Aviv',
country: 'Israel',
latitude: '32.109333',
longitude: '34.855499',
region: 'fra1',
});
});
});

View File

@@ -0,0 +1,45 @@
/**
* @jest-environment @edge-runtime/jest-environment
*/
import { next, rewrite } from '../src/middleware-helpers';
describe('rewrite', () => {
test('receives custom headers', () => {
const resp = rewrite(new URL('https://example.vercel.sh/'), {
headers: {
'x-custom-header': 'custom-value',
},
});
expect({
status: resp.status,
headers: Object.fromEntries(resp.headers),
}).toMatchObject({
status: 200,
headers: {
'x-custom-header': 'custom-value',
'x-middleware-rewrite': 'https://example.vercel.sh/',
},
});
});
});
describe('next', () => {
test('receives custom headers', () => {
const resp = next({
headers: {
'x-custom-header': 'custom-value',
},
});
expect({
status: resp.status,
headers: Object.fromEntries(resp.headers),
}).toMatchObject({
status: 200,
headers: {
'x-custom-header': 'custom-value',
'x-middleware-next': '1',
},
});
});
});

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"target": "ES2020",
"module": "commonjs",
"outDir": "dist",
"sourceMap": false,
"declaration": true,
"skipLibCheck": true,
"moduleResolution": "node",
"typeRoots": ["./@types", "./node_modules/@types"]
},
"include": ["src/**/*", "test/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "1.0.2-canary.0",
"version": "1.0.2",
"main": "./dist/frameworks.js",
"types": "./dist/frameworks.d.ts",
"files": [
@@ -21,7 +21,7 @@
"@types/js-yaml": "3.12.1",
"@types/node": "12.0.4",
"@types/node-fetch": "2.5.8",
"@vercel/routing-utils": "1.13.5-canary.0",
"@vercel/routing-utils": "1.13.5",
"ajv": "6.12.2",
"typescript": "4.3.4"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/go",
"version": "2.0.2-canary.1",
"version": "2.0.2",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
@@ -25,7 +25,7 @@
"@types/fs-extra": "^5.0.5",
"@types/node-fetch": "^2.3.0",
"@types/tar": "^4.0.0",
"@vercel/build-utils": "4.1.1-canary.1",
"@vercel/build-utils": "4.2.0",
"@vercel/ncc": "0.24.0",
"async-retry": "1.3.1",
"execa": "^1.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "3.0.3-canary.0",
"version": "3.1.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
@@ -45,9 +45,9 @@
"@types/semver": "6.0.0",
"@types/text-table": "0.2.1",
"@types/webpack-sources": "3.2.0",
"@vercel/build-utils": "4.1.1-canary.1",
"@vercel/build-utils": "4.2.0",
"@vercel/nft": "0.19.1",
"@vercel/routing-utils": "1.13.5-canary.0",
"@vercel/routing-utils": "1.13.5",
"async-sema": "3.0.1",
"buffer-crc32": "0.2.13",
"cheerio": "1.0.0-rc.10",

View File

@@ -44,7 +44,8 @@ export async function getNextjsEdgeFunctionSource(
* We validate at this point because we want to verify against user code.
* It should not count the Worker wrapper nor the Next.js wrapper.
*/
await validateScript(text);
const wasmFiles = (wasm ?? []).map(({ filePath }) => join(outputDir, filePath));
await validateSize(text, wasmFiles);
// Wrap to fake module.exports
const getPageMatchCode = `(function () {
@@ -72,11 +73,17 @@ function getWasmImportStatements(wasm: { name: string }[] = []) {
.join('\n');
}
async function validateScript(content: string) {
async function validateSize(script: string, wasmFiles: string[]) {
const buffers = [Buffer.from(script, 'utf8')];
for (const filePath of wasmFiles) {
buffers.push(await readFile(filePath));
}
const content = Buffer.concat(buffers);
const gzipped = await gzip(content);
if (gzipped.length > EDGE_FUNCTION_SIZE_LIMIT) {
throw new Error(
`Exceeds maximum edge function script size: ${bytes(
`Exceeds maximum edge function size: ${bytes(
gzipped.length
)} / ${bytes(EDGE_FUNCTION_SIZE_LIMIT)}`
);

View File

@@ -78,7 +78,6 @@ import {
updateRouteSrc,
validateEntrypoint,
} from './utils';
import assert from 'assert';
export const version = 2;
export const htmlContentType = 'text/html; charset=utf-8';
@@ -1529,7 +1528,7 @@ export const build: BuildV2 = async ({
pageTraces,
compressedPages,
tracedPseudoLayer?.pseudoLayer || {},
0,
{ pseudoLayer: {}, pseudoLayerBytes: 0 },
0,
lambdaCompressedByteLimit,
// internal pages are already referenced in traces for serverless
@@ -1545,7 +1544,7 @@ export const build: BuildV2 = async ({
pageTraces,
compressedPages,
tracedPseudoLayer?.pseudoLayer || {},
0,
{ pseudoLayer: {}, pseudoLayerBytes: 0 },
0,
lambdaCompressedByteLimit,
[]
@@ -1950,11 +1949,23 @@ export const build: BuildV2 = async ({
if (!currentPage) {
console.error(
"Failed to find matching page for", {toRender, header: req.headers['x-nextjs-page'], url: req.url }, "in lambda"
)
console.error('pages in lambda', Object.keys(pages))
res.statusCode = 500
return res.end('internal server error')
"pages in lambda:",
Object.keys(pages),
"page header received:",
req.headers["x-nextjs-page"]
);
throw new Error(
"Failed to find matching page in lambda for: " +
JSON.stringify(
{
toRender,
url: req.url,
header: req.headers["x-nextjs-page"],
},
null,
2
)
);
}
const mod = currentPage()
@@ -2601,15 +2612,6 @@ async function getServerlessPages(params: {
for (const edgeFunctionFile of Object.keys(
middlewareManifest?.functions ?? {}
)) {
// `getStaticProps` are expecting `Prerender` output which is a Serverless function
// and not an Edge Function. Therefore we only remove API endpoints for now, as they
// don't have `getStaticProps`.
//
// Context: https://github.com/vercel/vercel/pull/7905#discussion_r890213165
assert(
edgeFunctionFile.startsWith('/api/'),
`Only API endpoints are currently supported for Edge endpoints.`
);
delete pages[edgeFunctionFile.slice(1) + '.js'];
}

View File

@@ -425,11 +425,8 @@ export async function serverBuild({
const uncompressedInitialSize = Object.keys(
initialPseudoLayer.pseudoLayer
).reduce((prev, cur) => {
return (
prev +
(initialPseudoLayer.pseudoLayer[cur] as PseudoFile)
.uncompressedSize || 0
);
const file = initialPseudoLayer.pseudoLayer[cur] as PseudoFile;
return prev + file.uncompressedSize || 0;
}, 0);
debug(
@@ -611,13 +608,6 @@ export async function serverBuild({
}, {})
);
const initialPseudoLayerSize = Object.keys(
initialPseudoLayer.pseudoLayer
).reduce((prev, cur) => {
const file = initialPseudoLayer.pseudoLayer[cur] as PseudoFile;
return prev + file.uncompressedSize || 0;
}, 0);
const pageExtensions = requiredServerFilesManifest.config?.pageExtensions;
const pageLambdaGroups = await getPageLambdaGroups(
@@ -628,9 +618,9 @@ export async function serverBuild({
pageTraces,
compressedPages,
tracedPseudoLayer.pseudoLayer,
initialPseudoLayer.pseudoLayerBytes,
initialPseudoLayerSize,
initialPseudoLayer,
lambdaCompressedByteLimit,
uncompressedInitialSize,
internalPages,
pageExtensions
);
@@ -643,8 +633,8 @@ export async function serverBuild({
pageTraces,
compressedPages,
tracedPseudoLayer.pseudoLayer,
initialPseudoLayer.pseudoLayerBytes,
initialPseudoLayerSize,
initialPseudoLayer,
uncompressedInitialSize,
lambdaCompressedByteLimit,
internalPages
);
@@ -682,7 +672,6 @@ export async function serverBuild({
const lambda = await createLambdaFromPseudoLayers({
files: launcherFiles,
layers: [
initialPseudoLayer.pseudoLayer,
group.pseudoLayer,
[...group.pages, ...internalPages].reduce((prev, page) => {
const pageFileName = path.normalize(
@@ -797,6 +786,7 @@ export async function serverBuild({
outputDirectory,
routesManifest,
isCorrectMiddlewareOrder,
prerenderBypassToken: prerenderManifest.bypassToken || '',
});
const isNextDataServerResolving =
@@ -905,6 +895,20 @@ export async function serverBuild({
},
],
},
// normalize "/index" from "/_next/data/index.json" to -> just "/"
// as matches a rewrite sources will expect just "/"
{
src: path.join('^/', entryDirectory, '/index'),
has: [
{
type: 'header',
key: 'x-nextjs-data',
},
],
dest: path.join('/', entryDirectory),
...(isOverride ? { override: true } : {}),
continue: true,
},
]
: [];
};
@@ -913,7 +917,30 @@ export async function serverBuild({
return isNextDataServerResolving
? [
{
src: '/(.*)',
src: path.join('^/', entryDirectory, '$'),
has: [
{
type: 'header',
key: 'x-nextjs-data',
},
],
dest: `${path.join(
'/',
entryDirectory,
'/_next/data/',
buildId,
'/index.json'
)}`,
continue: true,
...(isOverride ? { override: true } : {}),
},
// handle non-trailing slash
{
src: path.join(
'^/',
entryDirectory,
'((?!_next/)(?:.*[^/]|.*))/?$'
),
has: [
{
type: 'header',

View File

@@ -1289,8 +1289,8 @@ export async function getPageLambdaGroups(
[page: string]: PseudoFile;
},
tracedPseudoLayer: PseudoLayer,
initialPseudoLayerSize: number,
initialPseudoLayerUncompressedSize: number,
initialPseudoLayer: PseudoLayerResult,
initialPseudoLayerUncompressed: number,
lambdaCompressedByteLimit: number,
internalPages: string[],
pageExtensions?: string[]
@@ -1341,10 +1341,10 @@ export async function getPageLambdaGroups(
}
const underUncompressedLimit =
newTracedFilesUncompressedSize + initialPseudoLayerUncompressedSize <
newTracedFilesUncompressedSize <
MAX_UNCOMPRESSED_LAMBDA_SIZE - LAMBDA_RESERVED_UNCOMPRESSED_SIZE;
const underCompressedLimit =
newTracedFilesSize + initialPseudoLayerSize <
newTracedFilesSize <
lambdaCompressedByteLimit - LAMBDA_RESERVED_COMPRESSED_SIZE;
return underUncompressedLimit && underCompressedLimit;
@@ -1359,9 +1359,9 @@ export async function getPageLambdaGroups(
pages: [page],
...opts,
isPrerenders: isPrerenderRoute,
pseudoLayerBytes: 0,
pseudoLayerUncompressedBytes: 0,
pseudoLayer: {},
pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes,
pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed,
pseudoLayer: Object.assign({}, initialPseudoLayer.pseudoLayer),
};
groups.push(newGroup);
matchingGroup = newGroup;
@@ -2142,9 +2142,11 @@ export async function getMiddlewareBundle({
outputDirectory,
routesManifest,
isCorrectMiddlewareOrder,
prerenderBypassToken,
}: {
entryPath: string;
outputDirectory: string;
prerenderBypassToken: string;
routesManifest: RoutesManifest;
isCorrectMiddlewareOrder: boolean;
}) {
@@ -2268,6 +2270,13 @@ export async function getMiddlewareBundle({
const route: Route = {
continue: true,
src: worker.routeSrc,
missing: [
{
type: 'header',
key: 'x-prerender-revalidate',
value: prerenderBypassToken,
},
],
};
if (worker.type === 'function') {

View File

@@ -77,6 +77,13 @@ export function middleware(request) {
return NextResponse.rewrite(url);
}
if (url.pathname === '/rewrite-to-site') {
const customUrl = new URL(url);
customUrl.pathname = '/_sites/subdomain-1/';
console.log('rewriting to', customUrl.pathname, customUrl.href);
return NextResponse.rewrite(customUrl);
}
if (url.pathname === '/redirect-me-to-about') {
url.pathname = '/about';
url.searchParams.set('middleware', 'foo');

View File

@@ -0,0 +1,31 @@
export default function Page(props) {
return (
<>
<p>/_sites/[site]</p>
<p>{JSON.stringify(props)}</p>
</>
);
}
export function getStaticProps({ params }) {
return {
props: {
params,
now: Date.now(),
},
};
}
export function getStaticPaths() {
return {
paths: [
{
params: { site: 'subdomain-1' },
},
{
params: { site: 'subdomain-2' },
},
],
fallback: 'blocking',
};
}

View File

@@ -2,6 +2,15 @@
"version": 2,
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
"probes": [
{
"path": "/_next/data/testing-build-id/rewrite-to-site.json",
"status": 200,
"headers": {
"x-nextjs-data": 1
},
"mustContain": "site\":\"subdomain-1\"",
"mustNotContain": "<html>"
},
{
"path": "/redirect-me",
"status": 307,

View File

@@ -715,7 +715,7 @@ it('Should provide lambda info when limit is hit (server build)', async () => {
console.log = origLog;
expect(logs).toContain(
'Max serverless function size was exceeded for 1 function'
'Max serverless function size was exceeded for 2 functions'
);
expect(logs).toContain(
'Max serverless function size of 50 MB compressed or 250 MB uncompressed reached'
@@ -802,3 +802,36 @@ it('Should provide lambda info when limit is hit for internal pages (server buil
expect(logs).toMatch(/public\/big-image-1\.jpg/);
expect(logs).toMatch(/public\/big-image-2\.jpg/);
});
it('Should provide lambda info when limit is hit (uncompressed)', async () => {
let logs = '';
const origLog = console.log;
console.log = function (...args) {
logs += args.join(' ');
origLog(...args);
};
try {
await runBuildLambda(
path.join(__dirname, 'test-limit-exceeded-404-static-files')
);
} catch (err) {
console.error(err);
}
console.log = origLog;
expect(logs).toContain(
'Max serverless function size was exceeded for 1 function'
);
expect(logs).toContain(
'Max serverless function size of 50 MB compressed or 250 MB uncompressed reached'
);
expect(logs).toContain(`Serverless Function's page: api/hello.js`);
expect(logs).toMatch(
/Large Dependencies.*?Uncompressed size.*?Compressed size/
);
expect(logs).toMatch(/data\.txt/);
expect(logs).toMatch(/\.next\/server\/pages/);
});

View File

@@ -105,6 +105,14 @@ function sharedTests(ctx: Context) {
const routes = ctx.buildResult.routes.filter(
route => 'middleware' in route || 'middlewarePath' in route
);
expect(
routes.every(
route =>
route.missing[0].type === 'header' &&
route.missing[0].key === 'x-prerender-revalidate' &&
route.missing[0].value.length > 0
)
).toBeTruthy();
expect(routes.length).toBeGreaterThan(0);
});
}

View File

@@ -0,0 +1,33 @@
const fs = require('fs');
const assert = require('assert');
const locales = ['en'];
let charStart = 97;
let charEnd = 105;
// generate 81 random locales under en
for (let i = charStart; i <= charEnd; i++) {
const firstChar = String.fromCharCode(i);
for (let j = charStart; j <= charEnd; j++) {
const secondChar = String.fromCharCode(j);
locales.push(`en-${firstChar}${secondChar}`);
}
}
assert(
locales.length === 82,
`unexpected locale count, expected 82, received ${locales.length}`
);
// generate 100MB text file which will be traced in `/api/hello`
// which when combined with the 404 HTML files will push us over the 250MB
// uncompressed limit
fs.writeFileSync('data.txt', new Array(100 * 1000 * 1000).fill('a').join());
module.exports = {
i18n: {
locales,
defaultLocale: 'en',
},
};

View File

@@ -0,0 +1,12 @@
{
"name": "test-limit",
"version": "1.0.0",
"scripts": {
"build": "next build"
},
"dependencies": {
"next": "canary",
"react": "17.0.2",
"react-dom": "17.0.2"
}
}

View File

@@ -0,0 +1,19 @@
export default function Page(props) {
return (
<>
<p>404 | Page Not Found</p>
<p>{JSON.stringify(props)}</p>
</>
);
}
export function getStaticProps({ locale }) {
return {
props: {
locale,
// 1MB string which is duplicated in HTML totalling 2MB
// this will be generated for each locale as well
largeData: new Array(1 * 1000 * 1000).fill('a').join(''),
},
};
}

View File

@@ -0,0 +1,16 @@
/* eslint-disable */
import React from 'react';
if (typeof window === 'undefined') {
try {
const fs = require('fs');
const path = require('path');
fs.readdirSync(path.join(process.cwd(), 'public'));
fs.readdirSync(path.join(process.cwd(), 'node_modules/chrome-aws-lambda'));
fs.readdirSync(path.join(process.cwd(), 'node_modules/firebase'));
} catch (_) {}
}
export default function MyApp({ Component, pageProps }) {
return React.createElement(Component, pageProps);
}

View File

@@ -0,0 +1,12 @@
import fs from 'fs';
import path from 'path';
try {
fs.readFileSync(path.join(process.cwd(), 'data.txt'));
} catch (_) {
/**/
}
export default function handler(req, res) {
res.end('hello');
}

View File

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

View File

@@ -0,0 +1,9 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
]
}

View File

@@ -1,5 +1,5 @@
import { basename, join, dirname } from 'path';
import { getNextjsEdgeFunctionSource } from '../../dist/edge-function-source/get-edge-function-source';
import { getNextjsEdgeFunctionSource } from '../../src/edge-function-source/get-edge-function-source';
import { nanoid } from 'nanoid';
import { tmpdir } from 'os';
import { writeFile } from 'fs-extra';
@@ -31,10 +31,49 @@ it('should throw an error when exceeds the script size limit', async () => {
dir
);
}).rejects.toThrow(
/Exceeds maximum edge function script size: .+[MK]B \/ .+[M|K]B/i
/Exceeds maximum edge function size: .+[MK]B \/ .+[M|K]B/i
);
});
it('throws an error if it contains too big WASM file', async () => {
const filepath = `${join(tmpdir(), nanoid())}.js`;
const file = basename(filepath);
const dir = dirname(filepath);
await writeFile(
filepath,
`
import wasm from './big.wasm?module';
module.exports.middleware = function () {
console.log(wasm)
return Response('hi')
}
`
);
const wasmPath = join(dir, 'big.wasm');
await writeFile(wasmPath, randomBytes(1200 * 1024));
expect(async () => {
await getNextjsEdgeFunctionSource(
[file],
{
name: 'middleware',
staticRoutes: [],
nextConfig: null,
},
dir,
[
{
name: 'wasm_big',
filePath: 'big.wasm',
},
]
);
}).rejects.toThrow(
/Exceeds maximum edge function size: .+[MK]B \/ .+[M|K]B/i
);
});
it('uses the template', async () => {
const filepath = `${join(tmpdir(), nanoid())}.js`;
const file = basename(filepath);
@@ -48,6 +87,9 @@ it('uses the template', async () => {
`
);
const wasmPath = join(dir, 'small.wasm');
await writeFile(wasmPath, randomBytes(8));
const edgeFunctionSource = await getNextjsEdgeFunctionSource(
[file],
{
@@ -58,12 +100,12 @@ it('uses the template', async () => {
dir,
[
{
name: 'wasm_1234',
filePath: 'server/middleware-chunks/wasm_1234.wasm',
name: 'wasm_small',
filePath: 'small.wasm',
},
]
);
const source = edgeFunctionSource.source();
expect(source).toMatch(/nextConfig/);
expect(source).toContain(`const wasm_1234 = require("/wasm/wasm_1234.wasm")`);
expect(source).toContain(`const wasm_small = require("/wasm/wasm_small.wasm")`);
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/node",
"version": "2.2.0",
"version": "2.3.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -31,7 +31,7 @@
},
"dependencies": {
"@types/node": "*",
"@vercel/build-utils": "4.1.1-canary.1",
"@vercel/build-utils": "4.2.0",
"@vercel/ncc": "0.24.0",
"@vercel/node-bridge": "3.0.0",
"@vercel/static-config": "2.0.1",
@@ -56,6 +56,7 @@
"content-type": "1.0.4",
"cookie": "0.4.0",
"etag": "1.8.1",
"path-to-regexp": "6.2.1",
"source-map-support": "0.5.12",
"test-listen": "1.1.0"
}

View File

@@ -74,7 +74,7 @@ import { Readable } from 'stream';
import type { Bridge } from '@vercel/node-bridge/bridge';
import { getVercelLauncher } from '@vercel/node-bridge/launcher.js';
import { VercelProxyResponse } from '@vercel/node-bridge/types';
import { streamToBuffer } from '@vercel/build-utils';
import { Config, streamToBuffer, debug } from '@vercel/build-utils';
import exitHook from 'exit-hook';
import { EdgeRuntime, Primitives, runServer } from 'edge-runtime';
import { getConfig } from '@vercel/static-config';
@@ -82,6 +82,17 @@ import { Project } from 'ts-morph';
import ncc from '@vercel/ncc';
import fetch from 'node-fetch';
function logError(error: Error) {
console.error(error.message);
if (error.stack) {
// only show the stack trace if debug is enabled
// because it points to internals, not user code
const errorPrefixLength = 'Error: '.length;
const errorMessageLength = errorPrefixLength + error.message.length;
debug(error.stack.substring(errorMessageLength + 1));
}
}
function listen(server: Server, port: number, host: string): Promise<void> {
return new Promise(resolve => {
server.listen(port, host, () => {
@@ -136,77 +147,151 @@ async function serializeRequest(message: IncomingMessage) {
});
}
async function compileUserCode(entrypoint: string) {
try {
const buildResult = await ncc(entrypoint, {
target: 'es2022',
});
const userCode = buildResult.code;
return `
${userCode};
addEventListener('fetch', async (event) => {
try {
let serializedRequest = await event.request.text();
let requestDetails = JSON.parse(serializedRequest);
let body;
if (requestDetails.method !== 'GET' && requestDetails.method !== 'HEAD') {
body = Uint8Array.from(atob(requestDetails.body), c => c.charCodeAt(0));
}
let requestUrl = requestDetails.headers['x-forwarded-proto'] + '://' + requestDetails.headers['x-forwarded-host'] + requestDetails.url;
let request = new Request(requestUrl, {
headers: requestDetails.headers,
method: requestDetails.method,
body: body
});
event.request = request;
let edgeHandler = module.exports.default;
if (!edgeHandler) {
throw new Error('No default export was found. Add a default export to handle requests.');
}
let response = await edgeHandler(event.request, event);
return event.respondWith(response);
} catch (error) {
// we can't easily show a meaningful stack trace
// so, stick to just the error message for now
event.respondWith(new Response(error.message, {
status: 500,
headers: {
'x-vercel-failed': 'edge-wrapper'
}
}));
}
})`;
} catch (error) {
// We can't easily show a meaningful stack trace from ncc -> edge-runtime.
// So, stick with just the message for now.
console.log(`Failed to instantiate edge runtime: ${error.message}`);
return undefined;
}
}
async function createEdgeRuntime(userCode: string | undefined) {
try {
if (!userCode) {
return undefined;
}
const edgeRuntime = new EdgeRuntime({
initialCode: userCode,
extend: (context: Primitives) => {
Object.assign(context, {
__dirname: '',
module: {
exports: {},
},
});
return context;
},
});
const server = await runServer({ runtime: edgeRuntime });
exitHook(server.close);
return server;
} catch (error) {
// We can't easily show a meaningful stack trace from ncc -> edge-runtime.
// So, stick with just the message for now.
console.log(`Failed to instantiate edge runtime: ${error.message}`);
return undefined;
}
}
async function createEdgeEventHandler(
entrypoint: string
): Promise<(request: IncomingMessage) => Promise<VercelProxyResponse>> {
const buildResult = await ncc(entrypoint, { target: 'es2022' });
const userCode = buildResult.code;
const initialCode = `
${userCode};
addEventListener('fetch', async (event) => {
let serializedRequest = await event.request.text();
let requestDetails = JSON.parse(serializedRequest);
let body;
if (requestDetails.method !== 'GET' && requestDetails.method !== 'HEAD') {
body = Uint8Array.from(atob(requestDetails.body), c => c.charCodeAt(0));
}
let requestUrl = requestDetails.headers['x-forwarded-proto'] + '://' + requestDetails.headers['x-forwarded-host'] + requestDetails.url;
let request = new Request(requestUrl, {
headers: requestDetails.headers,
method: requestDetails.method,
body: body
});
event.request = request;
let edgeHandler = module.exports.default;
let response = edgeHandler(event.request, event);
return event.respondWith(response);
})`;
const edgeRuntime = new EdgeRuntime({
initialCode,
extend: (context: Primitives) => {
Object.assign(context, {
__dirname: '',
module: {
exports: {},
},
});
return context;
},
});
const server = await runServer({ runtime: edgeRuntime });
exitHook(server.close);
const userCode = await compileUserCode(entrypoint);
const server = await createEdgeRuntime(userCode);
return async function (request: IncomingMessage) {
if (!server) {
// this error state is already logged, but we have to wait until here to exit the process
// this matches the serverless function bridge launcher's behavior when
// an error is thrown in the function
process.exit(1);
}
const response = await fetch(server.url, {
redirect: 'manual',
method: 'post',
body: await serializeRequest(request),
});
const body = await response.text();
const isUserError =
response.headers.get('x-vercel-failed') === 'edge-wrapper';
if (isUserError && response.status >= 500) {
// this error was "unhandled" from the user code's perspective
console.log(`Unhandled rejection: ${body}`);
// this matches the serverless function bridge launcher's behavior when
// an error is thrown in the function
process.exit(1);
}
return {
statusCode: response.status,
headers: response.headers.raw(),
body: await response.text(),
body,
encoding: 'utf8',
};
};
}
const validRuntimes = ['experimental-edge'];
function parseRuntime(entrypoint: string): string | undefined {
function parseRuntime(
entrypoint: string,
entryPointPath: string
): string | undefined {
const project = new Project();
const staticConfig = getConfig(project, entrypoint);
const staticConfig = getConfig(project, entryPointPath);
const runtime = staticConfig?.runtime;
if (runtime && !validRuntimes.includes(runtime)) {
throw new Error(`Invalid function runtime for "${entrypoint}": ${runtime}`);
throw new Error(
`Invalid function runtime "${runtime}" for "${entrypoint}". Valid runtimes are: ${JSON.stringify(
validRuntimes
)}`
);
}
return runtime;
@@ -214,17 +299,24 @@ function parseRuntime(entrypoint: string): string | undefined {
async function createEventHandler(
entrypoint: string,
config: Config,
options: { shouldAddHelpers: boolean }
): Promise<(request: IncomingMessage) => Promise<VercelProxyResponse>> {
const runtime = parseRuntime(entrypoint);
if (runtime === 'experimental-edge') {
return createEdgeEventHandler(entrypoint);
const entryPointPath = join(process.cwd(), entrypoint!);
const runtime = parseRuntime(entrypoint, entryPointPath);
// `middleware.js`/`middleware.ts` file is always run as
// an Edge Function, otherwise needs to be opted-in via
// `export const config = { runtime: 'experimental-edge' }`
if (config.middleware === true || runtime === 'experimental-edge') {
return createEdgeEventHandler(entryPointPath);
}
return createServerlessEventHandler(entrypoint, options);
return createServerlessEventHandler(entryPointPath, options);
}
let handleEvent: (request: IncomingMessage) => Promise<VercelProxyResponse>;
let handlerEventError: Error;
async function main() {
const config = JSON.parse(process.env.VERCEL_DEV_CONFIG || '{}');
@@ -240,8 +332,14 @@ async function main() {
const proxyServer = createServer(onDevRequest);
await listen(proxyServer, 0, '127.0.0.1');
const entryPointPath = join(process.cwd(), entrypoint!);
handleEvent = await createEventHandler(entryPointPath, { shouldAddHelpers });
try {
handleEvent = await createEventHandler(entrypoint!, config, {
shouldAddHelpers,
});
} catch (error) {
logError(error);
handlerEventError = error;
}
const address = proxyServer.address();
if (typeof process.send === 'function') {
@@ -270,6 +368,13 @@ export async function onDevRequest(
req: IncomingMessage,
res: ServerResponse
): Promise<void> {
if (handlerEventError) {
// this error state is already logged, but we have to wait until here to exit the process
// this matches the serverless function bridge launcher's behavior when
// an error is thrown in the function
process.exit(1);
}
if (!handleEvent) {
res.statusCode = 500;
res.end('Bridge is not ready, please try again');
@@ -307,6 +412,6 @@ export function fixConfigDev(config: { compilerOptions: any }): void {
}
main().catch(err => {
console.error(err);
logError(err);
process.exit(1);
});

View File

@@ -50,6 +50,7 @@ import type {
import { getConfig } from '@vercel/static-config';
import { Register, register } from './typescript';
import { getRegExpFromMatchers } from './utils';
export { shouldServe };
export type {
@@ -381,40 +382,48 @@ export const build: BuildV3 = async ({
? handler.substring(0, handler.length - 3)
: handler;
const isMiddleware = config.middleware === true;
// Will output an `EdgeFunction` for when `config.middleware = true`
// (i.e. for root-level "middleware" file) or if source code contains:
// `export const config = { runtime: 'experimental-edge' }`
let isEdgeFunction = false;
let isEdgeFunction = isMiddleware;
const project = new Project();
const staticConfig = getConfig(project, entrypointPath);
if (staticConfig?.runtime) {
if (!ALLOWED_RUNTIMES.includes(staticConfig.runtime)) {
throw new Error(
`Unsupported "runtime" property in \`config\`: ${JSON.stringify(
staticConfig.runtime
)} (must be one of: ${JSON.stringify(ALLOWED_RUNTIMES)})`
);
}
isEdgeFunction = staticConfig.runtime === 'experimental-edge';
}
// Add a `route` for Middleware
if (isMiddleware) {
if (!isEdgeFunction) {
// Root-level middleware file can not have `export const config = { runtime: 'nodejs' }`
throw new Error(
`Middleware file can not be a Node.js Serverless Function`
);
}
// Middleware is a catch-all for all paths unless a `matcher` property is defined
const src = getRegExpFromMatchers(staticConfig?.matcher);
// Add a catch-all `route` for Middleware
if (config.middleware === true) {
routes = [
{
src: '/.*',
src,
middlewarePath: config.zeroConfig
? outputName
: relative(baseDir, entrypointPath),
continue: true,
override: true,
},
];
// Middleware is implicitly an Edge Function
isEdgeFunction = true;
}
if (!isEdgeFunction) {
const project = new Project();
const staticConfig = getConfig(project, entrypointPath);
if (staticConfig?.runtime) {
if (!ALLOWED_RUNTIMES.includes(staticConfig.runtime)) {
throw new Error(
`Unsupported "runtime" property in \`config\`: ${JSON.stringify(
staticConfig.runtime
)} (must be one of: ${JSON.stringify(ALLOWED_RUNTIMES)})`
);
}
isEdgeFunction = staticConfig.runtime === 'experimental-edge';
}
}
if (isEdgeFunction) {
@@ -451,7 +460,26 @@ export const prepareCache: PrepareCache = ({ repoRootPath, workPath }) => {
export const startDevServer: StartDevServer = async opts => {
const { entrypoint, workPath, config, meta = {} } = opts;
const entryDir = join(workPath, dirname(entrypoint));
const entrypointPath = join(workPath, entrypoint);
if (config.middleware === true && typeof meta.requestUrl === 'string') {
// TODO: static config is also parsed in `dev-server.ts`.
// we should pass in this version as an env var instead.
const project = new Project();
const staticConfig = getConfig(project, entrypointPath);
// Middleware is a catch-all for all paths unless a `matcher` property is defined
const matchers = new RegExp(getRegExpFromMatchers(staticConfig?.matcher));
if (!matchers.test(meta.requestUrl)) {
// If the "matchers" doesn't say to handle this
// path then skip middleware invocation
return null;
}
}
const entryDir = dirname(entrypointPath);
const projectTsConfig = await walkParentDirs({
base: workPath,
start: entryDir,
@@ -483,6 +511,12 @@ export const startDevServer: StartDevServer = async opts => {
});
const { pid } = child;
if (!pid) {
throw new Error(
`Child Process has no "pid" when forking: "${devServerPath}"`
);
}
const onMessage = once<{ port: number }>(child, 'message');
const onExit = once.spread<[number, string | null]>(child, 'exit');
const result = await Promise.race([onMessage, onExit]);
@@ -505,7 +539,7 @@ export const startDevServer: StartDevServer = async opts => {
// Got "exit" event from child process
const [exitCode, signal] = result;
const reason = signal ? `"${signal}" signal` : `exit code ${exitCode}`;
throw new Error(`\`node ${entrypoint}\` failed with ${reason}`);
throw new Error(`Function \`${entrypoint}\` failed with ${reason}`);
}
};
@@ -513,7 +547,7 @@ async function doTypeCheck(
{ entrypoint, workPath, meta = {} }: StartDevServerOptions,
projectTsConfig: string | null
): Promise<void> {
const { devCacheDir = join(workPath, '.now', 'cache') } = meta;
const { devCacheDir = join(workPath, '.vercel', 'cache') } = meta;
const entrypointCacheDir = join(devCacheDir, 'node', entrypoint);
// In order to type-check a single file, a standalone tsconfig

View File

@@ -0,0 +1,26 @@
import { pathToRegexp } from 'path-to-regexp';
export function getRegExpFromMatchers(matcherOrMatchers: unknown): string {
if (!matcherOrMatchers) {
return '^/.*$';
}
const matchers = Array.isArray(matcherOrMatchers)
? matcherOrMatchers
: [matcherOrMatchers];
return matchers.map(getRegExpFromMatcher).join('|');
}
function getRegExpFromMatcher(matcher: unknown): string {
if (typeof matcher !== 'string') {
throw new Error(
'`matcher` must be a path matcher or an array of path matchers'
);
}
if (!matcher.startsWith('/')) {
throw new Error('`matcher`: path matcher must start with /');
}
const re = pathToRegexp(matcher);
return re.source;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/python",
"version": "3.0.2-canary.1",
"version": "3.0.2",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -23,7 +23,7 @@
"devDependencies": {
"@types/execa": "^0.9.0",
"@types/jest": "27.4.1",
"@vercel/build-utils": "4.1.1-canary.1",
"@vercel/build-utils": "4.2.0",
"@vercel/ncc": "0.24.0",
"execa": "^1.0.0",
"typescript": "4.3.4"

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/redwood",
"version": "1.0.2-canary.1",
"version": "1.0.2",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://vercel.com/docs",
@@ -21,13 +21,13 @@
},
"dependencies": {
"@vercel/nft": "0.19.1",
"@vercel/routing-utils": "1.13.5-canary.0",
"@vercel/routing-utils": "1.13.5",
"semver": "6.1.1"
},
"devDependencies": {
"@types/aws-lambda": "8.10.19",
"@types/node": "*",
"@types/semver": "6.0.0",
"@vercel/build-utils": "4.1.1-canary.1"
"@vercel/build-utils": "4.2.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/remix",
"version": "1.0.2-canary.1",
"version": "1.0.2",
"license": "MIT",
"main": "./dist/index.js",
"homepage": "https://vercel.com/docs",
@@ -21,13 +21,12 @@
"default-server.js"
],
"dependencies": {
"@remix-run/vercel": "1.4.3",
"@vercel/nft": "0.19.1"
},
"devDependencies": {
"@types/jest": "27.5.1",
"@types/node": "*",
"@vercel/build-utils": "4.1.1-canary.1",
"@vercel/build-utils": "4.2.0",
"typescript": "4.6.4"
}
}

View File

@@ -13,6 +13,7 @@ import {
runNpmInstall,
runPackageJsonScript,
scanParentDirs,
walkParentDirs,
} from '@vercel/build-utils';
import type {
BuildV2,
@@ -23,10 +24,17 @@ import type {
import { nodeFileTrace } from '@vercel/nft';
import type { AppConfig } from './types';
// Name of the Remix runtime adapter npm package for Vercel
const REMIX_RUNTIME_ADAPTER_NAME = '@remix-run/vercel';
// Pinned version of the last verified working version of the adapter
const REMIX_RUNTIME_ADAPTER_VERSION = '1.6.1';
export const build: BuildV2 = async ({
entrypoint,
files,
workPath,
repoRootPath,
config,
meta = {},
}) => {
@@ -70,6 +78,51 @@ export const build: BuildV2 = async ({
}
}
// Ensure `@remix-run/vercel` is in the project's `package.json`
const packageJsonPath = await walkParentDirs({
base: repoRootPath,
start: workPath,
filename: 'package.json',
});
if (packageJsonPath) {
const packageJson: PackageJson = JSON.parse(
await fs.readFile(packageJsonPath, 'utf8')
);
const { dependencies = {}, devDependencies = {} } = packageJson;
let modified = false;
if (REMIX_RUNTIME_ADAPTER_NAME in devDependencies) {
dependencies[REMIX_RUNTIME_ADAPTER_NAME] =
devDependencies[REMIX_RUNTIME_ADAPTER_NAME];
delete devDependencies[REMIX_RUNTIME_ADAPTER_NAME];
console.log(
`Warning: Moving "${REMIX_RUNTIME_ADAPTER_NAME}" from \`devDependencies\` to \`dependencies\`. You should commit this change.`
);
modified = true;
} else if (!(REMIX_RUNTIME_ADAPTER_NAME in dependencies)) {
dependencies[REMIX_RUNTIME_ADAPTER_NAME] = REMIX_RUNTIME_ADAPTER_VERSION;
console.log(
`Warning: Adding "${REMIX_RUNTIME_ADAPTER_NAME}" v${REMIX_RUNTIME_ADAPTER_VERSION} to \`dependencies\`. You should commit this change.`
);
modified = true;
}
if (modified) {
const packageJsonString = JSON.stringify(
{
...packageJson,
dependencies,
devDependencies,
},
null,
2
);
await fs.writeFile(packageJsonPath, `${packageJsonString}\n`);
}
} else {
debug(`Failed to find "package.json" file in project`);
}
if (typeof installCommand === 'string') {
if (installCommand.trim()) {
console.log(`Running "install" command: \`${installCommand}\`...`);
@@ -215,11 +268,8 @@ async function createRenderFunction(
base: rootDir,
});
let needsVercelAdapter = false;
for (const warning of trace.warnings) {
if (warning.message.includes("'@remix-run/vercel'")) {
needsVercelAdapter = true;
} else if (warning.stack) {
if (warning.stack) {
debug(warning.stack.replace('Error: ', 'Warning: '));
}
}
@@ -227,33 +277,6 @@ async function createRenderFunction(
files[file] = await FileFsRef.fromFsPath({ fsPath: join(rootDir, file) });
}
if (needsVercelAdapter) {
// Package in the Builder's version of `@remix-run/vercel` Runtime adapter
const remixVercelPackageJsonPath = require.resolve(
'@remix-run/vercel/package.json',
{
paths: [__dirname],
}
);
const remixVercelPackageJson: PackageJson = require(remixVercelPackageJsonPath);
const remixVercelDir = dirname(remixVercelPackageJsonPath);
const remixVercelEntrypoint = join(remixVercelDir, 'index.js');
console.log(
`Warning: Implicitly adding \`${remixVercelPackageJson.name}\` v${remixVercelPackageJson.version} to your project. You should add this dependency to your \`package.json\` file.`
);
const adapterBase = join(remixVercelDir, '../../..');
const adapterTrace = await nodeFileTrace([remixVercelEntrypoint], {
base: adapterBase,
});
for (const file of adapterTrace.fileList) {
files[file] = await FileFsRef.fromFsPath({
fsPath: join(adapterBase, file),
});
}
}
const lambda = new NodejsLambda({
files,
handler,

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