Compare commits

...

25 Commits

Author SHA1 Message Date
Nathan Rajlich
547e88228e Publish Stable
- @vercel/build-utils@4.2.1
 - vercel@25.2.3
 - @vercel/client@12.0.3
 - @vercel/go@2.0.3
 - @vercel/next@3.1.2
 - @vercel/node@2.3.3
 - @vercel/python@3.0.3
 - @vercel/redwood@1.0.4
 - @vercel/remix@1.0.4
 - @vercel/ruby@1.3.11
 - @vercel/static-build@1.0.3
2022-06-30 12:24:13 -07:00
Luc Leray
9bfb5dd535 [build-utils] Handle npm bin exit code 7 (#8058)
In some rare cases, `npm bin` exits with code 7, but still outputs the right bin path.

To reproduce, try:
```
npm init -y
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
vc
# enter "echo build" for the build command, leave the other configuration as default
```

The build will fail with `Error: Command exited with 7` because `npm bin` fails with code 7, for some reason.

In this PR, we do 2 things:
(1) Ignore exit codes from `npm bin`. It still outputs the right path when it exits with code 7 so we just read the output and check if it's a valid path.
(2) Throw a more specific error message when `npm bin` fails to give us the bin path. The current error was hard to debug because it looked like it was coming from the install commmand. We can do better by emitting a custom error.

Alternative considered for (2): Do not throw errors. If `npm bin` fails, emit a warning and let the build continue.

Related Issues:
- https://github.com/vercel/customer-issues/issues/585 (internal)
2022-06-30 17:27:52 +00:00
Nathan Rajlich
81ea84fae8 [cli] Fix vc build lambda serialization when there's a broken symlink (#8050)
There's some cleanup directory walking logic that was choking when
a Lambda outputs a file with a broken symlink. We shouldn't need to
traverse into those directories in the case of a symlink anyways, so use
`lstat()` instead of `stat()` to prevent that filesystem call from
throwing an error.
2022-06-29 16:04:31 -07:00
Nathan Rajlich
fa8bf07be4 [cli] Add Client#stdin / Client#stdout (#8039)
This will allow for mockability of the input streams (i.e. prompts)
for CLI commands in unit tests.

**Example:**

```typescript
import confirm from '../../src/util/input/confirm';
import { client } from '../mocks/client';

describe('MockClient', () => {
  it('should mock `confirm()`', async () => {
    const confirmedPromise = confirm(client, 'Do the thing?', false);

    client.stdin.write('yes\n');

    const confirmed = await confirmedPromise;
    expect(confirmed).toEqual(true);
  });
});
```
2022-06-29 16:03:56 -07:00
JJ Kasper
cc9dce73ad [next] Ensure uncompressed limit is correct (#8049)
* Revert "Revert "[next] Update max size warning to handle initial layer better" (#8047)"

This reverts commit 8c62de16ce.

* Ensure uncompressed limit is correct

* apply suggestion
2022-06-29 15:13:15 -05:00
Sean Massa
bba7cbd411 [cli][dev] fix: creating "api/some-func.js" after "vc dev" now works (#8041)
If there is no `api` directory, then you run `vc dev`, then you create a new function `api/some-func.js`, then this file would not be served as a new function.

This was being caused by incomplete "new file" handling logic. This PR ensures that the proper detection is done in each new file (`getVercelConfig`) that populates key properties (`apiDir`, `apiExtensions`, and extensionless `files`) for determining when a file is used to serve a request.
2022-06-29 18:37:48 +00:00
Steven
9a3739bebd Publish Stable
- vercel@25.2.2
 - @vercel/next@3.1.1
 - @vercel/node@2.3.2
 - @vercel/redwood@1.0.3
 - @vercel/remix@1.0.3
2022-06-29 09:27:34 -04:00
Gal Schlezinger
8c62de16ce Revert "[next] Update max size warning to handle initial layer better" (#8047)
Revert "[next] Update max size warning to handle initial layer better (#8013)"

This reverts commit f20703b15d.
2022-06-29 09:26:29 -04:00
Steven
e9333988d7 [next][node][redwood][remix] Bump @vercel/nft to 0.20.1 (#8042)
- https://github.com/vercel/nft/releases/tag/0.20.0
- https://github.com/vercel/nft/releases/tag/0.20.1
2022-06-29 00:21:14 +00:00
Nathan Rajlich
fb001ce7eb [tests] Remove TODO comments from Middleware matchers vc dev test (#8037) 2022-06-28 18:26:56 +00:00
Sean Massa
b399fe7037 Publish Stable
- vercel@25.2.1
 - @vercel/node@2.3.1
2022-06-28 12:04:57 -05:00
Sean Massa
88385b3c84 [cli][node] switch to esbuild for compiling edge functions (#8032) 2022-06-28 11:27:56 -05:00
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
114 changed files with 2467 additions and 1134 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

@@ -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.1",
"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

@@ -61,6 +61,13 @@ export interface SpawnOptionsExtended extends SpawnOptions {
* Pretty formatted command that is being spawned for logging purposes.
*/
prettyCommand?: string;
/**
* Returns instead of throwing an error when the process exits with a
* non-0 exit code. When relevant, the returned object will include
* the error code, stdout and stderr.
*/
ignoreNon0Exit?: boolean;
}
export function spawnAsync(
@@ -79,7 +86,7 @@ export function spawnAsync(
child.on('error', reject);
child.on('close', (code, signal) => {
if (code === 0) {
if (code === 0 || opts.ignoreNon0Exit) {
return resolve();
}
@@ -123,24 +130,24 @@ export function execAsync(
child.on('error', reject);
child.on('close', (code, signal) => {
if (code !== 0) {
const cmd = opts.prettyCommand
? `Command "${opts.prettyCommand}"`
: 'Command';
return reject(
new NowBuildError({
code: `BUILD_UTILS_EXEC_${code || signal}`,
message: `${cmd} exited with ${code || signal}`,
})
);
if (code === 0 || opts.ignoreNon0Exit) {
return resolve({
code,
stdout: Buffer.concat(stdoutList).toString(),
stderr: Buffer.concat(stderrList).toString(),
});
}
return resolve({
code,
stdout: Buffer.concat(stdoutList).toString(),
stderr: Buffer.concat(stderrList).toString(),
});
const cmd = opts.prettyCommand
? `Command "${opts.prettyCommand}"`
: 'Command';
return reject(
new NowBuildError({
code: `BUILD_UTILS_EXEC_${code || signal}`,
message: `${cmd} exited with ${code || signal}`,
})
);
});
}
);
@@ -166,9 +173,30 @@ export async function execCommand(command: string, options: SpawnOptions = {}) {
return true;
}
export async function getNodeBinPath({ cwd }: { cwd: string }) {
const { stdout } = await execAsync('npm', ['bin'], { cwd });
return stdout.trim();
export async function getNodeBinPath({
cwd,
}: {
cwd: string;
}): Promise<string | undefined> {
const { code, stdout, stderr } = await execAsync('npm', ['bin'], {
cwd,
prettyCommand: 'npm bin',
// in some rare cases, we saw `npm bin` exit with a non-0 code, but still
// output the right bin path, so we ignore the exit code
ignoreNon0Exit: true,
});
const nodeBinPath = stdout.trim();
if (path.isAbsolute(nodeBinPath)) {
return nodeBinPath;
}
throw new NowBuildError({
code: `BUILD_UTILS_GET_NODE_BIN_PATH`,
message: `Running \`npm bin\` failed to return a valid bin path (code=${code}, stdout=${stdout}, stderr=${stderr})`,
});
}
async function chmodPlusX(fsPath: string) {

View File

@@ -0,0 +1,29 @@
import { execAsync, NowBuildError } from '../src';
it('should execute a command', async () => {
const { code, stdout, stderr } = await execAsync('echo', ['hello']);
expect(code).toBe(0);
expect(stdout).toContain('hello');
expect(stderr).toBe('');
});
it('should throw if the command exits with non-0 code', async () => {
await expect(execAsync('find', ['unknown-file'])).rejects.toBeInstanceOf(
NowBuildError
);
});
it('should return if the command exits with non-0 code and ignoreNon0Exit=true', async () => {
const { code, stdout, stderr } = await execAsync('find', ['unknown-file'], {
ignoreNon0Exit: true,
});
expect(code).toBe(process.platform === 'win32' ? 2 : 1);
expect(stdout).toBe('');
expect(stderr).toContain(
process.platform === 'win32'
? 'Parameter format not correct'
: 'No such file or directory'
);
});

View File

@@ -0,0 +1,21 @@
import { spawnAsync, NowBuildError } from '../src';
it('should execute a command', async () => {
// should resolve (it doesn't return anything, so it resolves with "undefined")
await expect(spawnAsync('echo', ['hello'])).resolves.toBeUndefined();
});
it('should throw if the command exits with non-0 code', async () => {
await expect(spawnAsync('find', ['unknown-file'])).rejects.toBeInstanceOf(
NowBuildError
);
});
it('should return if the command exits with non-0 code and ignoreNon0Exit=true', async () => {
// should resolve (it doesn't return anything, so it resolves with "undefined")
await expect(
spawnAsync('find', ['unknown-file'], {
ignoreNon0Exit: true,
})
).resolves.toBeUndefined();
});

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "25.1.1-canary.10",
"version": "25.2.3",
"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.5-canary.0",
"@vercel/node": "2.2.1-canary.1",
"@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.1",
"@vercel/go": "2.0.3",
"@vercel/next": "3.1.2",
"@vercel/node": "2.3.3",
"@vercel/python": "3.0.3",
"@vercel/redwood": "1.0.4",
"@vercel/remix": "1.0.4",
"@vercel/ruby": "1.3.11",
"@vercel/static-build": "1.0.3",
"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.3",
"@vercel/frameworks": "1.0.2",
"@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2",

View File

@@ -10,7 +10,6 @@ import confirm from '../../util/input/confirm';
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
import { Alias } from '../../types';
import { Output } from '../../util/output';
import { isValidName } from '../../util/is-valid-name';
import { getCommandName } from '../../util/pkg-name';
@@ -71,7 +70,7 @@ export default async function rm(
}
const removeStamp = stamp();
if (!opts['--yes'] && !(await confirmAliasRemove(output, alias))) {
if (!opts['--yes'] && !(await confirmAliasRemove(client, alias))) {
output.log('Aborted');
return 0;
}
@@ -85,7 +84,7 @@ export default async function rm(
return 0;
}
async function confirmAliasRemove(output: Output, alias: Alias) {
async function confirmAliasRemove(client: Client, alias: Alias) {
const srcUrl = alias.deployment
? chalk.underline(alias.deployment.url)
: null;
@@ -104,7 +103,7 @@ async function confirmAliasRemove(output: Output, alias: Alias) {
}
);
output.log(`The following alias will be removed permanently`);
output.print(` ${tbl}\n`);
return confirm(chalk.red('Are you sure?'), false);
client.output.log(`The following alias will be removed permanently`);
client.output.print(` ${tbl}\n`);
return confirm(client, chalk.red('Are you sure?'), false);
}

View File

@@ -187,6 +187,7 @@ export default async client => {
if (cardId) {
const label = `Are you sure that you to set this card as the default?`;
const confirmation = await promptBool(label, {
...client,
trailing: '\n',
});
@@ -262,7 +263,7 @@ export default async client => {
// typed `vercel billing rm <some-id>`) is valid
if (cardId) {
const label = `Are you sure that you want to remove this card?`;
const confirmation = await promptBool(label);
const confirmation = await promptBool(label, client);
if (!confirmation) {
console.log('Aborted');
break;

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

@@ -140,6 +140,7 @@ export default async function main(client: Client): Promise<number> {
}
confirmed = await confirm(
client,
`No Project Settings found locally. Run ${cli.getCommandName(
'pull'
)} for retrieving them?`,

View File

@@ -165,7 +165,7 @@ export default async (client: Client) => {
const quiet = !isTTY;
// check paths
const pathValidation = await validatePaths(output, paths);
const pathValidation = await validatePaths(client, paths);
if (!pathValidation.valid) {
return pathValidation.exitCode;
@@ -243,6 +243,7 @@ export default async (client: Client) => {
const shouldStartSetup =
autoConfirm ||
(await confirm(
client,
`Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
@@ -287,7 +288,7 @@ export default async (client: Client) => {
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
rootDirectory = await inputRootDirectory(client, path, autoConfirm);
} else {
project = projectOrNewProjectName;
rootDirectory = project.rootDirectory;
@@ -521,7 +522,7 @@ export default async (client: Client) => {
}
const settings = await editProjectSettings(
output,
client,
projectSettings,
framework,
false,

View File

@@ -46,7 +46,7 @@ export default async function add(
const addStamp = stamp();
const { domain, data: argData } = parsedParams;
const data = await getDNSData(output, argData);
const data = await getDNSData(client, argData);
if (!data) {
output.log(`Aborted`);
return 1;

View File

@@ -87,7 +87,8 @@ export default async function buy(
!(await promptBool(
`Buy now for ${chalk.bold(`$${price}`)} (${`${period}yr${
period > 1 ? 's' : ''
}`})?`
}`})?`,
client
))
) {
return 0;
@@ -99,7 +100,7 @@ export default async function buy(
: `Auto renew every ${renewalPrice.period} years for ${chalk.bold(
`$${price}`
)}?`,
{ defaultValue: true }
{ ...client, defaultValue: true }
);
let buyResult;

View File

@@ -77,7 +77,8 @@ export default async function move(
!(await promptBool(
`Are you sure you want to move ${param(domainName)} to ${param(
destination
)}?`
)}?`,
client
))
) {
output.log('Aborted');
@@ -95,7 +96,8 @@ export default async function move(
);
if (
!(await promptBool(
`Are you sure you want to move ${param(domainName)}?`
`Are you sure you want to move ${param(domainName)}?`,
client
))
) {
output.log('Aborted');

View File

@@ -92,7 +92,10 @@ export default async function rm(
const skipConfirmation = opts['--yes'] || false;
if (
!skipConfirmation &&
!(await promptBool(`Are you sure you want to remove ${param(domainName)}?`))
!(await promptBool(
`Are you sure you want to remove ${param(domainName)}?`,
client
))
) {
output.log('Aborted');
return 0;
@@ -230,7 +233,7 @@ async function removeDomain(
if (
!skipConfirmation &&
!(await promptBool(`Remove conflicts associated with domain?`))
!(await promptBool(`Remove conflicts associated with domain?`, client))
) {
output.log('Aborted');
return 0;

View File

@@ -81,7 +81,8 @@ export default async function transferIn(
const shouldTransfer = await promptBool(
transferPolicy === 'no-change'
? `Transfer now for ${chalk.bold(`$${price}`)}?`
: `Transfer now with 1yr renewal for ${chalk.bold(`$${price}`)}?`
: `Transfer now with 1yr renewal for ${chalk.bold(`$${price}`)}?`,
client
);
if (!shouldTransfer) {
return 0;

View File

@@ -31,7 +31,7 @@ export default async function add(
// improve the way we show inquirer prompts
require('../../util/input/patch-inquirer');
const stdInput = await readStandardInput();
const stdInput = await readStandardInput(client.stdin);
let [envName, envTargetArg, envGitBranch] = args;
if (args.length > 3) {

View File

@@ -74,6 +74,7 @@ export default async function pull(
exists &&
!skipConfirmation &&
!(await confirm(
client,
`Found existing file ${param(filename)}. Do you want to overwrite?`,
false
))

View File

@@ -104,6 +104,7 @@ export default async function rm(
if (
!skipConfirmation &&
!(await confirm(
client,
`Removing Environment Variable ${param(env.key)} from ${formatEnvTarget(
env
)} in Project ${chalk.bold(project.name)}. Are you sure?`,

View File

@@ -65,7 +65,7 @@ export default async function init(
return extractExample(client, name, dir, force, 'v1');
}
const found = await guess(exampleList, name);
const found = await guess(client, exampleList, name);
if (typeof found === 'string') {
return extractExample(client, found, dir, force);
@@ -194,7 +194,7 @@ function prepareFolder(cwd: string, folder: string, force?: boolean) {
/**
* Guess which example user try to init
*/
async function guess(exampleList: string[], name: string) {
async function guess(client: Client, exampleList: string[], name: string) {
const GuessError = new Error(
`No example found for ${chalk.bold(name)}, run ${getCommandName(
`init`
@@ -208,7 +208,7 @@ async function guess(exampleList: string[], name: string) {
const found = didYouMean(name, exampleList, 0.7);
if (typeof found === 'string') {
if (await promptBool(`Did you mean ${chalk.bold(found)}?`)) {
if (await promptBool(`Did you mean ${chalk.bold(found)}?`, client)) {
return found;
}
} else {

View File

@@ -387,6 +387,8 @@ const main = async () => {
// Shared API `Client` instance for all sub-commands to utilize
client = new Client({
apiUrl,
stdin: process.stdin,
stdout: process.stdout,
output,
config,
authConfig,

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

@@ -385,8 +385,14 @@ export async function* findDirs(
}
for (const path of paths) {
const abs = join(dir, path);
const s = await fs.stat(abs);
if (s.isDirectory()) {
let stat: fs.Stats;
try {
stat = await fs.lstat(abs);
} catch (err: any) {
if (err.code === 'ENOENT') continue;
throw err;
}
if (stat.isDirectory()) {
if (path === name) {
yield relative(root, abs);
} else {

View File

@@ -32,6 +32,8 @@ export interface ClientOptions {
argv: string[];
apiUrl: string;
authConfig: AuthConfig;
stdin: NodeJS.ReadStream;
stdout: NodeJS.WriteStream;
output: Output;
config: GlobalConfig;
localConfig?: VercelConfig;
@@ -45,6 +47,8 @@ export default class Client extends EventEmitter {
argv: string[];
apiUrl: string;
authConfig: AuthConfig;
stdin: NodeJS.ReadStream;
stdout: NodeJS.WriteStream;
output: Output;
config: GlobalConfig;
localConfig?: VercelConfig;
@@ -55,6 +59,8 @@ export default class Client extends EventEmitter {
this.argv = opts.argv;
this.apiUrl = opts.apiUrl;
this.authConfig = opts.authConfig;
this.stdin = opts.stdin;
this.stdout = opts.stdout;
this.output = opts.output;
this.config = opts.config;
this.localConfig = opts.localConfig;

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')
@@ -327,6 +329,8 @@ export default class DevServer {
): Promise<void> {
const name = relative(this.cwd, fsPath);
try {
await this.getVercelConfig();
this.files[name] = await FileFsRef.fromFsPath({ fsPath });
const extensionless = this.getExtensionlessFile(name);
if (extensionless) {
@@ -593,7 +597,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 +1110,7 @@ export default class DevServer {
view = errorTemplate({
http_status_code: statusCode,
http_status_description,
error_code,
request_id: requestId,
});
}
@@ -1337,32 +1342,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 +1408,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 +1552,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 +1837,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 +2285,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 +2363,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 +2390,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

@@ -2,26 +2,29 @@ import chalk from 'chalk';
import { DNSRecordData } from '../../types';
import textInput from '../input/text';
import promptBool from '../input/prompt-bool';
import { Output } from '../output';
import Client from '../client';
const RECORD_TYPES = ['A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'SRV', 'TXT'];
export default async function getDNSData(
output: Output,
client: Client,
data: null | DNSRecordData
): Promise<DNSRecordData | null> {
if (data) {
return data;
}
const { output } = client;
try {
// first ask for type, branch from there
const possibleTypes = new Set(RECORD_TYPES);
const type = (await textInput({
label: `- Record type (${RECORD_TYPES.join(', ')}): `,
validateValue: (v: string) =>
Boolean(v && possibleTypes.has(v.trim().toUpperCase()))
}))
const type = (
await textInput({
label: `- Record type (${RECORD_TYPES.join(', ')}): `,
validateValue: (v: string) =>
Boolean(v && possibleTypes.has(v.trim().toUpperCase())),
})
)
.trim()
.toUpperCase();
@@ -39,7 +42,7 @@ export default async function getDNSData(
target
)}.`
);
return (await verifyData())
return (await verifyData(client))
? {
name,
type,
@@ -47,8 +50,8 @@ export default async function getDNSData(
priority,
weight,
port,
target
}
target,
},
}
: null;
}
@@ -61,23 +64,23 @@ export default async function getDNSData(
`${mxPriority}`
)} ${chalk.cyan(value)}`
);
return (await verifyData())
return (await verifyData(client))
? {
name,
type,
value,
mxPriority
mxPriority,
}
: null;
}
const value = await getTrimmedString(`- ${type} value: `);
output.log(`${chalk.cyan(name)} ${chalk.bold(type)} ${chalk.cyan(value)}`);
return (await verifyData())
return (await verifyData(client))
? {
name,
type,
value
value,
}
: null;
} catch (error) {
@@ -85,13 +88,13 @@ export default async function getDNSData(
}
}
async function verifyData() {
return promptBool('Is this correct?');
async function verifyData(client: Client) {
return promptBool('Is this correct?', client);
}
async function getRecordName(type: string) {
const input = await textInput({
label: `- ${type} name: `
label: `- ${type} name: `,
});
return input === '@' ? '' : input;
}
@@ -100,14 +103,14 @@ async function getNumber(label: string) {
return Number(
await textInput({
label,
validateValue: v => Boolean(v && Number(v))
validateValue: v => Boolean(v && Number(v)),
})
);
}
async function getTrimmedString(label: string) {
const res = await textInput({
label,
validateValue: v => Boolean(v && v.trim().length > 0)
validateValue: v => Boolean(v && v.trim().length > 0),
});
return res.trim();
}

View File

@@ -54,7 +54,8 @@ export default async function purchaseDomainIfAvailable(
!(await promptBool(
`Buy ${chalk.underline(domain)} for ${chalk.bold(
`$${price}`
)} (${plural('yr', period, true)})?`
)} (${plural('yr', period, true)})?`,
client
))
) {
output.print(eraseLines(1));

View File

@@ -1,12 +1,19 @@
import inquirer from 'inquirer';
import Client from '../client';
export default async function confirm(
client: Client,
message: string,
preferred: boolean
): Promise<boolean> {
require('./patch-inquirer');
const answers = await inquirer.prompt({
const prompt = inquirer.createPromptModule({
input: client.stdin,
output: client.stdout,
});
const answers = await prompt({
type: 'confirm',
name: 'value',
message,

View File

@@ -1,8 +1,8 @@
import inquirer from 'inquirer';
import confirm from './confirm';
import chalk from 'chalk';
import { Output } from '../output';
import frameworkList, { Framework } from '@vercel/frameworks';
import Client from '../client';
import { isSettingValue } from '../is-setting-value';
import { ProjectSettings } from '../../types';
@@ -22,12 +22,14 @@ const settingKeys = Object.keys(settingMap).sort() as unknown as readonly [
export type PartialProjectSettings = Pick<ProjectSettings, ConfigKeys>;
export default async function editProjectSettings(
output: Output,
client: Client,
projectSettings: PartialProjectSettings | null,
framework: Framework | null,
autoConfirm: boolean,
localConfigurationOverrides: PartialProjectSettings | null
): Promise<ProjectSettings> {
const { output } = client;
// Create initial settings object defaulting everything to `null` and assigning what may exist in `projectSettings`
const settings: ProjectSettings = Object.assign(
{
@@ -118,7 +120,7 @@ export default async function editProjectSettings(
// Prompt the user if they want to modify any settings not defined by local configuration.
if (
autoConfirm ||
!(await confirm('Want to modify these settings?', false))
!(await confirm(client, 'Want to modify these settings?', false))
) {
return settings;
}

View File

@@ -47,11 +47,16 @@ export default async function inputProject(
if (!detectedProject) {
// did not auto-detect a project to link
shouldLinkProject = await confirm(`Link to existing project?`, false);
shouldLinkProject = await confirm(
client,
`Link to existing project?`,
false
);
} else {
// auto-detected a project to link
if (
await confirm(
client,
`Found project ${chalk.cyan(
`${org.slug}/${detectedProject.name}`
)}. Link to it?`,
@@ -63,6 +68,7 @@ export default async function inputProject(
// user doesn't want to link the auto-detected project
shouldLinkProject = await confirm(
client,
`Link to different existing project?`,
true
);
@@ -73,7 +79,11 @@ export default async function inputProject(
let project: Project | ProjectNotFound | null = null;
while (!project || project instanceof ProjectNotFound) {
const answers = await inquirer.prompt({
const prompt = inquirer.createPromptModule({
input: client.stdin,
output: client.stdout,
});
const answers = await prompt({
type: 'input',
name: 'existingProjectName',
message: `Whats the name of your existing project?`,

View File

@@ -1,12 +1,12 @@
import path from 'path';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { Output } from '../output';
import { validateRootDirectory } from '../validate-paths';
import Client from '../client';
export async function inputRootDirectory(
client: Client,
cwd: string,
output: Output,
autoConfirm = false
) {
if (autoConfirm) {
@@ -15,7 +15,11 @@ export async function inputRootDirectory(
// eslint-disable-next-line no-constant-condition
while (true) {
const { rootDirectory } = await inquirer.prompt({
const prompt = inquirer.createPromptModule({
input: client.stdin,
output: client.stdout,
});
const { rootDirectory } = await prompt({
type: 'input',
name: 'rootDirectory',
message: `In which directory is your code located?`,
@@ -38,7 +42,7 @@ export async function inputRootDirectory(
if (
(await validateRootDirectory(
output,
client.output,
cwd,
fullPath,
'Please choose a different one.'

View File

@@ -5,21 +5,21 @@ type Options = {
defaultValue?: boolean;
noChar?: string;
resolveChars?: Set<string>;
stdin?: NodeJS.ReadStream;
stdout?: NodeJS.WriteStream;
stdin: NodeJS.ReadStream;
stdout: NodeJS.WriteStream;
trailing?: string;
yesChar?: string;
};
export default async function promptBool(label: string, options: Options = {}) {
export default async function promptBool(label: string, options: Options) {
const {
stdin,
stdout,
defaultValue = false,
abortSequences = new Set(['\u0003']),
resolveChars = new Set(['\r']),
yesChar = 'y',
noChar = 'n',
stdin = process.stdin,
stdout = process.stdout,
trailing = '',
} = options;

View File

@@ -1,13 +1,15 @@
export default async function readStandardInput(): Promise<string> {
export default async function readStandardInput(
stdin: NodeJS.ReadStream
): Promise<string> {
return new Promise<string>(resolve => {
setTimeout(() => resolve(''), 500);
if (process.stdin.isTTY) {
if (stdin.isTTY) {
// found tty so we know there is nothing piped to stdin
resolve('');
} else {
process.stdin.setEncoding('utf8');
process.stdin.once('data', resolve);
stdin.setEncoding('utf8');
stdin.once('data', resolve);
}
});
}

View File

@@ -80,6 +80,7 @@ export default async function setupAndLink(
const shouldStartSetup =
autoConfirm ||
(await confirm(
client,
`${setupMsg} ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
@@ -120,7 +121,7 @@ export default async function setupAndLink(
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
rootDirectory = await inputRootDirectory(client, path, autoConfirm);
} else {
const project = projectOrNewProjectName;
@@ -224,7 +225,7 @@ export default async function setupAndLink(
const { projectSettings, framework } = deployment;
settings = await editProjectSettings(
output,
client,
projectSettings,
framework,
autoConfirm,

View File

@@ -14,7 +14,7 @@ export default async function reauthenticate(
client.output.log(
`You must re-authenticate with SAML to use ${bold(error.scope)} scope.`
);
if (await confirm(`Log in with SAML?`, true)) {
if (await confirm(client, `Log in with SAML?`, true)) {
return doSamlLogin(client, error.teamId);
}
} else {

View File

@@ -5,6 +5,7 @@ import chalk from 'chalk';
import { homedir } from 'os';
import confirm from './input/confirm';
import toHumanPath from './humanize-path';
import Client from './client';
const stat = promisify(lstatRaw);
@@ -51,9 +52,11 @@ export async function validateRootDirectory(
}
export default async function validatePaths(
output: Output,
client: Client,
paths: string[]
): Promise<{ valid: true; path: string } | { valid: false; exitCode: number }> {
const { output } = client;
// can't deploy more than 1 path
if (paths.length > 1) {
output.print(`${chalk.red('Error!')} Can't deploy more than one path.\n`);
@@ -85,6 +88,7 @@ export default async function validatePaths(
// ask confirmation if the directory is home
if (path === homedir()) {
const shouldDeployHomeDirectory = await confirm(
client,
`You are deploying your home directory. Do you want to continue?`,
false
);

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, 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

@@ -0,0 +1,3 @@
{
"version": 2
}

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,235 @@ 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 { stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stderr).toMatch(/Failed to instantiate edge runtime./g);
expect(stderr).toMatch(/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 { stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stderr).toMatch(/Failed to instantiate edge runtime./g);
expect(stderr).toMatch(/Unexpected end of file/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 { stderr } = await dev.kill('SIGTERM');
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stderr).toMatch(
/Could not resolve "unknown-module-893427589372458934795843"/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 missing handler 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

@@ -1,7 +1,17 @@
// eslint-disable-next-line
import path from 'path';
import { join } from 'path';
import ms from 'ms';
import fs, { mkdirp } from 'fs-extra';
const { exec, fixture, testFixture, testFixtureStdio } = require('./utils.js');
const {
exec,
fetch,
fixture,
sleep,
testFixture,
testFixtureStdio,
validateResponseHeaders,
} = require('./utils.js');
test('[vercel dev] validate redirects', async () => {
const directory = fixture('invalid-redirects');
@@ -334,3 +344,44 @@ test(
await testPath(200, '/', /A simple deployment with the Vercel API!/m);
})
);
test(
'[vercel dev] add a `api/fn.ts` when `api` does not exist at startup`',
testFixtureStdio('no-api', async (_testPath: any, port: any) => {
const directory = fixture('no-api');
const apiDir = join(directory, 'api');
try {
{
const response = await fetch(`http://localhost:${port}/api/new-file`);
validateResponseHeaders(response);
expect(response.status).toBe(404);
}
const fileContents = `
export const config = {
runtime: 'experimental-edge'
}
export default async function edge(request, event) {
return new Response('from new file');
}
`;
await mkdirp(apiDir);
await fs.writeFile(join(apiDir, 'new-file.js'), fileContents);
// Wait until file events have been processed
await sleep(ms('1s'));
{
const response = await fetch(`http://localhost:${port}/api/new-file`);
validateResponseHeaders(response);
const body = await response.text();
expect(body.trim()).toBe('from new file');
}
} finally {
await fs.remove(apiDir);
}
})
);

View File

@@ -433,3 +433,85 @@ 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) => {
await testPath(404, '/');
await testPath(404, '/another');
await testPath(200, '/about/page', 'middleware response');
await testPath(200, '/dashboard/home', 'middleware response');
})
);

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

@@ -1,4 +1,5 @@
import chalk from 'chalk';
import { PassThrough } from 'stream';
import { createServer, Server } from 'http';
import express, { Express, Router } from 'express';
import listen from 'async-listen';
@@ -23,10 +24,13 @@ export class MockClient extends Client {
// Gets populated in `startMockServer()`
apiUrl: '',
authConfig: {},
stdin: new PassThrough(),
stdout: new PassThrough(),
output: new Output(),
config: {},
localConfig: {},
});
this.mockOutput = jest.fn();
this.app = express();
@@ -53,6 +57,12 @@ export class MockClient extends Client {
}
reset() {
this.stdin = new PassThrough();
this.stdin.isTTY = true;
this.stdout = new PassThrough();
this.stdout.isTTY = true;
this.output = new Output();
this.mockOutput = jest.fn();
this.output.print = s => {

View File

@@ -0,0 +1,25 @@
import confirm from '../../src/util/input/confirm';
import { client } from '../mocks/client';
describe('MockClient', () => {
it('should mock `confirm()`', async () => {
// true
let confirmedPromise = confirm(client, 'Do the thing?', false);
client.stdin.write('yes\n');
client.stdout.setEncoding('utf8');
client.stdout.on('data', d => console.log({ d }));
let confirmed = await confirmedPromise;
expect(confirmed).toEqual(true);
// false
confirmedPromise = confirm(client, 'Do the thing?', false);
client.stdin.write('no\n');
confirmed = await confirmedPromise;
expect(confirmed).toEqual(false);
});
});

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,13 +1,6 @@
import { Framework, frameworks } from '@vercel/frameworks';
import editProjectSettings from '../../../../src/util/input/edit-project-settings';
import { Output } from '../../../../src/util/output';
let output: Output;
beforeEach(() => {
output = new Output();
output.print = jest.fn();
});
import { client } from '../../../mocks/client';
const otherFramework = frameworks.find(
fwk => fwk.name === 'Other'
@@ -20,7 +13,7 @@ describe('editProjectSettings', () => {
describe('with no settings, "Other" framework, and no overrides provided', () => {
test('should default all settings to `null` and print user default framework settings', async () => {
const settings = await editProjectSettings(
output,
client,
null,
otherFramework,
true,
@@ -34,22 +27,14 @@ describe('editProjectSettings', () => {
installCommand: null,
outputDirectory: null,
});
expect((output.print as jest.Mock).mock.calls.length).toBe(5);
expect((output.print as jest.Mock).mock.calls[0][0]).toMatch(
expect(client.mockOutput.mock.calls.length).toBe(5);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/No framework detected. Default Project Settings:/
);
expect((output.print as jest.Mock).mock.calls[1][0]).toMatch(
/Build Command/
);
expect((output.print as jest.Mock).mock.calls[2][0]).toMatch(
/Development Command/
);
expect((output.print as jest.Mock).mock.calls[3][0]).toMatch(
/Install Command/
);
expect((output.print as jest.Mock).mock.calls[4][0]).toMatch(
/Output Directory/
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Development Command/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(/Install Command/);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Output Directory/);
});
});
@@ -63,29 +48,21 @@ describe('editProjectSettings', () => {
outputDirectory: 'OUTPUT_DIRECTORY',
};
const settings = await editProjectSettings(
output,
client,
projectSettings,
otherFramework,
true,
null
);
expect(settings).toStrictEqual({ ...projectSettings, framework: null });
expect((output.print as jest.Mock).mock.calls.length).toBe(5);
expect((output.print as jest.Mock).mock.calls[0][0]).toMatch(
expect(client.mockOutput.mock.calls.length).toBe(5);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/No framework detected. Default Project Settings:/
);
expect((output.print as jest.Mock).mock.calls[1][0]).toMatch(
/Build Command/
);
expect((output.print as jest.Mock).mock.calls[2][0]).toMatch(
/Development Command/
);
expect((output.print as jest.Mock).mock.calls[3][0]).toMatch(
/Install Command/
);
expect((output.print as jest.Mock).mock.calls[4][0]).toMatch(
/Output Directory/
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Development Command/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(/Install Command/);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Output Directory/);
});
});
@@ -99,28 +76,20 @@ describe('editProjectSettings', () => {
outputDirectory: 'OUTPUT_DIRECTORY',
};
const settings = await editProjectSettings(
output,
client,
projectSettings,
nextJSFramework,
true,
null
);
expect((output.print as jest.Mock).mock.calls.length).toBe(5);
expect((output.print as jest.Mock).mock.calls[0][0]).toMatch(
expect(client.mockOutput.mock.calls.length).toBe(5);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/Auto-detected Project Settings/
);
expect((output.print as jest.Mock).mock.calls[1][0]).toMatch(
/Build Command/
);
expect((output.print as jest.Mock).mock.calls[2][0]).toMatch(
/Development Command/
);
expect((output.print as jest.Mock).mock.calls[3][0]).toMatch(
/Install Command/
);
expect((output.print as jest.Mock).mock.calls[4][0]).toMatch(
/Output Directory/
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Development Command/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(/Install Command/);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Output Directory/);
expect(settings).toStrictEqual({
...projectSettings,
framework: nextJSFramework.slug,
@@ -146,38 +115,28 @@ describe('editProjectSettings', () => {
outputDirectory: 'OUTPUT_DIRECTORY',
};
const settings = await editProjectSettings(
output,
client,
projectSettings,
nextJSFramework,
true,
overrides
);
expect((output.print as jest.Mock).mock.calls.length).toBe(9);
expect((output.print as jest.Mock).mock.calls[0][0]).toMatch(
expect(client.mockOutput.mock.calls.length).toBe(9);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/Local settings detected in vercel.json:/
);
expect((output.print as jest.Mock).mock.calls[1][0]).toMatch(
/Build Command:/
);
expect((output.print as jest.Mock).mock.calls[2][0]).toMatch(
/Ignore Command:/
);
expect((output.print as jest.Mock).mock.calls[3][0]).toMatch(
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command:/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Ignore Command:/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(
/Development Command:/
);
expect((output.print as jest.Mock).mock.calls[4][0]).toMatch(
/Framework:/
);
expect((output.print as jest.Mock).mock.calls[5][0]).toMatch(
/Install Command:/
);
expect((output.print as jest.Mock).mock.calls[6][0]).toMatch(
/Output Directory:/
);
expect((output.print as jest.Mock).mock.calls[7][0]).toMatch(
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Framework:/);
expect(client.mockOutput.mock.calls[5][0]).toMatch(/Install Command:/);
expect(client.mockOutput.mock.calls[6][0]).toMatch(/Output Directory:/);
expect(client.mockOutput.mock.calls[7][0]).toMatch(
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
);
expect((output.print as jest.Mock).mock.calls[8][0]).toMatch(
expect(client.mockOutput.mock.calls[8][0]).toMatch(
/Auto-detected Project Settings/
);
@@ -196,38 +155,28 @@ describe('editProjectSettings', () => {
outputDirectory: 'OUTPUT_DIRECTORY',
};
const settings = await editProjectSettings(
output,
client,
null,
nextJSFramework,
true,
overrides
);
expect((output.print as jest.Mock).mock.calls.length).toBe(9);
expect((output.print as jest.Mock).mock.calls[0][0]).toMatch(
expect(client.mockOutput.mock.calls.length).toBe(9);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/Local settings detected in vercel.json:/
);
expect((output.print as jest.Mock).mock.calls[1][0]).toMatch(
/Build Command:/
);
expect((output.print as jest.Mock).mock.calls[2][0]).toMatch(
/Ignore Command:/
);
expect((output.print as jest.Mock).mock.calls[3][0]).toMatch(
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command:/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Ignore Command:/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(
/Development Command:/
);
expect((output.print as jest.Mock).mock.calls[4][0]).toMatch(
/Framework:/
);
expect((output.print as jest.Mock).mock.calls[5][0]).toMatch(
/Install Command:/
);
expect((output.print as jest.Mock).mock.calls[6][0]).toMatch(
/Output Directory:/
);
expect((output.print as jest.Mock).mock.calls[7][0]).toMatch(
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Framework:/);
expect(client.mockOutput.mock.calls[5][0]).toMatch(/Install Command:/);
expect(client.mockOutput.mock.calls[6][0]).toMatch(/Output Directory:/);
expect(client.mockOutput.mock.calls[7][0]).toMatch(
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
);
expect((output.print as jest.Mock).mock.calls[8][0]).toMatch(
expect(client.mockOutput.mock.calls[8][0]).toMatch(
/Auto-detected Project Settings/
);
expect(settings).toStrictEqual(overrides);
@@ -245,38 +194,28 @@ describe('editProjectSettings', () => {
outputDirectory: 'OUTPUT_DIRECTORY',
};
const settings = await editProjectSettings(
output,
client,
null,
null,
true,
overrides
);
expect((output.print as jest.Mock).mock.calls.length).toBe(9);
expect((output.print as jest.Mock).mock.calls[0][0]).toMatch(
expect(client.mockOutput.mock.calls.length).toBe(9);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/Local settings detected in vercel.json:/
);
expect((output.print as jest.Mock).mock.calls[1][0]).toMatch(
/Build Command:/
);
expect((output.print as jest.Mock).mock.calls[2][0]).toMatch(
/Ignore Command:/
);
expect((output.print as jest.Mock).mock.calls[3][0]).toMatch(
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command:/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Ignore Command:/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(
/Development Command:/
);
expect((output.print as jest.Mock).mock.calls[4][0]).toMatch(
/Framework:/
);
expect((output.print as jest.Mock).mock.calls[5][0]).toMatch(
/Install Command:/
);
expect((output.print as jest.Mock).mock.calls[6][0]).toMatch(
/Output Directory:/
);
expect((output.print as jest.Mock).mock.calls[7][0]).toMatch(
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Framework:/);
expect(client.mockOutput.mock.calls[5][0]).toMatch(/Install Command:/);
expect(client.mockOutput.mock.calls[6][0]).toMatch(/Output Directory:/);
expect(client.mockOutput.mock.calls[7][0]).toMatch(
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
);
expect((output.print as jest.Mock).mock.calls[8][0]).toMatch(
expect(client.mockOutput.mock.calls[8][0]).toMatch(
/Auto-detected Project Settings/
);

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "12.0.2-canary.1",
"version": "12.0.3",
"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.1",
"@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.3",
"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.1",
"@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.5-canary.0",
"version": "3.1.2",
"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/nft": "0.19.1",
"@vercel/routing-utils": "1.13.5-canary.0",
"@vercel/build-utils": "4.2.1",
"@vercel/nft": "0.20.1",
"@vercel/routing-utils": "1.13.5",
"async-sema": "3.0.1",
"buffer-crc32": "0.2.13",
"cheerio": "1.0.0-rc.10",

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';
@@ -1521,35 +1520,35 @@ export const build: BuildV2 = async ({
const pageLambdaGroups: Array<LambdaGroup> = [];
if (isSharedLambdas) {
const initialPageLambdaGroups = await getPageLambdaGroups(
const initialPageLambdaGroups = await getPageLambdaGroups({
entryPath,
config,
nonApiPages,
new Set(),
pages: nonApiPages,
prerenderRoutes: new Set(),
pageTraces,
compressedPages,
tracedPseudoLayer?.pseudoLayer || {},
0,
0,
tracedPseudoLayer: tracedPseudoLayer?.pseudoLayer || {},
initialPseudoLayer: { pseudoLayer: {}, pseudoLayerBytes: 0 },
initialPseudoLayerUncompressed: 0,
lambdaCompressedByteLimit,
// internal pages are already referenced in traces for serverless
// like builds
[]
);
internalPages: [],
});
const initialApiLambdaGroups = await getPageLambdaGroups(
const initialApiLambdaGroups = await getPageLambdaGroups({
entryPath,
config,
apiPages,
new Set(),
pages: apiPages,
prerenderRoutes: new Set(),
pageTraces,
compressedPages,
tracedPseudoLayer?.pseudoLayer || {},
0,
0,
tracedPseudoLayer: tracedPseudoLayer?.pseudoLayer || {},
initialPseudoLayer: { pseudoLayer: {}, pseudoLayerBytes: 0 },
initialPseudoLayerUncompressed: 0,
lambdaCompressedByteLimit,
[]
);
internalPages: [],
});
debug(
JSON.stringify(
@@ -2613,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,43 +608,36 @@ 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(
requiredServerFilesManifest.appDir || entryPath,
const pageLambdaGroups = await getPageLambdaGroups({
entryPath: requiredServerFilesManifest.appDir || entryPath,
config,
nonApiPages,
pages: nonApiPages,
prerenderRoutes,
pageTraces,
compressedPages,
tracedPseudoLayer.pseudoLayer,
initialPseudoLayer.pseudoLayerBytes,
initialPseudoLayerSize,
tracedPseudoLayer: tracedPseudoLayer.pseudoLayer,
initialPseudoLayer,
lambdaCompressedByteLimit,
initialPseudoLayerUncompressed: uncompressedInitialSize,
internalPages,
pageExtensions,
});
const apiLambdaGroups = await getPageLambdaGroups({
entryPath: requiredServerFilesManifest.appDir || entryPath,
config,
pages: apiPages,
prerenderRoutes,
pageTraces,
compressedPages,
tracedPseudoLayer: tracedPseudoLayer.pseudoLayer,
initialPseudoLayer,
initialPseudoLayerUncompressed: uncompressedInitialSize,
lambdaCompressedByteLimit,
internalPages,
pageExtensions
);
const apiLambdaGroups = await getPageLambdaGroups(
requiredServerFilesManifest.appDir || entryPath,
config,
apiPages,
prerenderRoutes,
pageTraces,
compressedPages,
tracedPseudoLayer.pseudoLayer,
initialPseudoLayer.pseudoLayerBytes,
initialPseudoLayerSize,
lambdaCompressedByteLimit,
internalPages
);
});
debug(
JSON.stringify(
@@ -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(

View File

@@ -1275,26 +1275,39 @@ export const MAX_UNCOMPRESSED_LAMBDA_SIZE = 250 * 1000 * 1000; // 250MB
const LAMBDA_RESERVED_UNCOMPRESSED_SIZE = 2.5 * 1000 * 1000; // 2.5MB
const LAMBDA_RESERVED_COMPRESSED_SIZE = 250 * 1000; // 250KB
export async function getPageLambdaGroups(
entryPath: string,
config: Config,
pages: string[],
prerenderRoutes: Set<string>,
export async function getPageLambdaGroups({
entryPath,
config,
pages,
prerenderRoutes,
pageTraces,
compressedPages,
tracedPseudoLayer,
initialPseudoLayer,
initialPseudoLayerUncompressed,
lambdaCompressedByteLimit,
internalPages,
pageExtensions,
}: {
entryPath: string;
config: Config;
pages: string[];
prerenderRoutes: Set<string>;
pageTraces: {
[page: string]: {
[key: string]: FileFsRef;
};
},
};
compressedPages: {
[page: string]: PseudoFile;
},
tracedPseudoLayer: PseudoLayer,
initialPseudoLayerSize: number,
initialPseudoLayerUncompressedSize: number,
lambdaCompressedByteLimit: number,
internalPages: string[],
pageExtensions?: string[]
) {
};
tracedPseudoLayer: PseudoLayer;
initialPseudoLayer: PseudoLayerResult;
initialPseudoLayerUncompressed: number;
lambdaCompressedByteLimit: number;
internalPages: string[];
pageExtensions?: string[];
}) {
const groups: Array<LambdaGroup> = [];
for (const page of pages) {
@@ -1341,10 +1354,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 +1372,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;

View File

@@ -105,6 +105,21 @@ it('should build using server build', async () => {
log.includes('WARNING: Unable to find source file for page')
)
).toBeFalsy();
const lambdas = new Set();
let totalLambdas = 0;
for (const item of Object.values(output)) {
if (item.type === 'Lambda') {
totalLambdas += 1;
lambdas.add(item);
} else if (item.type === 'Prerender') {
lambdas.add(item.lambda);
totalLambdas += 1;
}
}
expect(lambdas.size).toBe(5);
expect(lambdas.size).toBeLessThan(totalLambdas);
});
it('should build custom error lambda correctly', async () => {
@@ -715,7 +730,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 +817,99 @@ 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/);
});
it('Should de-dupe correctly when limit is close (uncompressed)', async () => {
const origLog = console.log;
const origError = console.error;
const caughtLogs = [];
console.log = function (...args) {
caughtLogs.push(args.join(' '));
origLog.apply(this, args);
};
console.error = function (...args) {
caughtLogs.push(args.join(' '));
origError.apply(this, args);
};
const {
buildResult: { output },
} = await runBuildLambda(
path.join(__dirname, 'test-limit-large-uncompressed-files')
);
console.log = origLog;
console.error = origError;
expect(output['index']).toBeDefined();
expect(output['another']).toBeDefined();
expect(output['api/hello']).toBeDefined();
expect(output['api/hello-1']).toBeDefined();
expect(output['api/hello-2']).toBeDefined();
expect(output['api/hello-3']).toBeDefined();
expect(output['api/hello-4']).toBeDefined();
expect(output['_app']).not.toBeDefined();
expect(output['_error']).not.toBeDefined();
expect(output['_document']).not.toBeDefined();
expect(output['index'] === output['another']).toBe(true);
expect(output['index'] !== output['api/hello']).toBe(true);
expect(output['api/hello'] === output['api/hello-1']).toBe(true);
expect(output['api/hello'] === output['api/hello-2']).toBe(true);
expect(output['api/hello'] === output['api/hello-3']).toBe(true);
expect(output['api/hello'] === output['api/hello-4']).toBe(true);
expect(
caughtLogs.some(log =>
log.includes('WARNING: Unable to find source file for page')
)
).toBeFalsy();
const lambdas = new Set();
let totalLambdas = 0;
for (const item of Object.values(output)) {
if (item.type === 'Lambda') {
totalLambdas += 1;
lambdas.add(item);
} else if (item.type === 'Prerender') {
lambdas.add(item.lambda);
totalLambdas += 1;
}
}
expect(lambdas.size).toBe(2);
expect(lambdas.size).toBeLessThan(totalLambdas);
});

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

@@ -0,0 +1,6 @@
const fs = require('fs');
// generate 200MB file which will be traced in `/api/hello`
fs.writeFileSync('data.txt', Buffer.alloc(200 * 1024 * 1024));
module.exports = {};

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,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,10 @@
export default function Home() {
return 'another page';
}
export function getServerSideProps() {
require('fs').readFileSync(require('path').join(process.cwd(), 'data.txt'));
return {
props: {},
};
}

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,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,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,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');
}

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