Files
vercel/packages/cli/test/dev/integration-1.test.ts
Kiko Beats 6dded87426 [node] Add streaming support for vc dev (#9745)
Until now, the user code response it's buffered and serialized. This is
mismatching how Vercel works these days.

This PR enables streaming response in `vc dev` for Edge/Serverless.

As part of the implementation, the `node-bridge` which spawns a process
to consume the user code is not necessary anymore.

Some necessary files (like HTTP server helpers) have been moved to live
in node builder package instead.

---------

Co-authored-by: Ethan Arrowood <ethan.arrowood@vercel.com>
Co-authored-by: Sean Massa <EndangeredMassa@gmail.com>
2023-04-19 23:56:41 +02:00

1089 lines
31 KiB
TypeScript

import os from 'os';
import url from 'url';
import fs from 'fs-extra';
import { join } from 'path';
import listen from 'async-listen';
import stripAnsi from 'strip-ansi';
import { createServer } from 'http';
const {
exec,
fetch,
fixture,
testFixture,
testFixtureStdio,
validateResponseHeaders,
} = require('./utils.js');
test('[verdel dev] should support serverless functions', async () => {
const dir = fixture('serverless-function');
const { dev, port, readyResolver } = await testFixture(dir, {});
try {
await readyResolver;
let res = await fetch(`http://localhost:${port}/api?foo=bar`);
validateResponseHeaders(res);
const payload = await res.json();
expect(payload).toMatchObject({ url: '/api?foo=bar', method: 'GET' });
expect(payload.headers.host).toBe(payload.headers['x-forwarded-host']);
} finally {
await dev.kill();
}
});
test('[vercel dev] should support edge functions', async () => {
const dir = fixture('edge-function');
const { dev, port, readyResolver } = await testFixture(dir, {
env: {
ENV_VAR_IN_EDGE: '1',
},
});
try {
await readyResolver;
const body = { hello: 'world' };
let res = await fetch(`http://localhost:${port}/api/edge-success`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
validateResponseHeaders(res);
// support for edge functions has to manually ensure that these properties
// are set up; so, we test that they are all passed through properly
const payload = await res.json();
expect(payload).toMatchObject({
headers: { 'content-type': 'application/json' },
url: `http://localhost:${port}/api/edge-success`,
method: 'POST',
body: '{"hello":"world"}',
snakeCase: 'some_camel_case_thing',
upperCase: 'SOMETHING',
optionalChaining: 'fallback',
ENV_VAR_IN_EDGE: '1',
});
expect(payload.headers.host).toBe(payload.headers['x-forwarded-host']);
} finally {
await dev.kill();
}
});
test('[vercel dev] edge functions support WebAssembly files', async () => {
const dir = fixture('edge-function');
const { dev, port, readyResolver } = await testFixture(dir, {
env: {
ENV_VAR_IN_EDGE: '1',
},
});
try {
await readyResolver;
for (const { number, result } of [
{ number: 1, result: 2 },
{ number: 2, result: 3 },
{ number: 12, result: 13 },
]) {
let res = await fetch(
`http://localhost:${port}/api/webassembly?number=${number}`
);
validateResponseHeaders(res);
await expect(res.text()).resolves.toEqual(`${number} + 1 = ${result}`);
}
} finally {
await dev.kill();
}
});
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');
await testPath(200, '/api/edge-import-browser');
})
);
test('[vercel dev] throws an error when an edge function has no response', 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-no-response`);
validateResponseHeaders(res);
const { stdout } = await dev.kill();
expect(await res.status).toBe(500);
expect(await res.text()).toMatch('FUNCTION_INVOCATION_FAILED');
expect(stdout).toMatch(
/Error from API Route \/api\/edge-no-response: Edge Function did not return a response./g
);
} finally {
await dev.kill();
}
});
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();
}
});
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 } = await dev.kill();
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stdout).toMatch(
/Error from API Route \/api\/edge-error-runtime: intentional runtime error/g
);
} finally {
await dev.kill();
}
});
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();
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: \["edge","experimental-edge"\]/g
);
} finally {
await dev.kill();
}
});
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();
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);
} finally {
await dev.kill();
}
});
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();
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stderr).toMatch(/Failed to compile user code for edge runtime./g);
expect(stderr).toMatch(/Unexpected end of file/g);
} finally {
await dev.kill();
}
});
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();
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stderr).toMatch(
/Could not resolve "unknown-module-893427589372458934795843"/g
);
} finally {
await dev.kill();
}
});
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 { stderr } = await dev.kill();
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stderr).toMatch(
/No default export was found. Add a default export to handle requests./g
);
} finally {
await dev.kill();
}
});
test('[vercel dev] should handle invalid middleware config', async () => {
const dir = fixture('middleware-matchers-invalid');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(`http://localhost:${port}/api/whatever`, {
method: 'GET',
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
});
validateResponseHeaders(res);
const { stderr } = await dev.kill();
expect(await res.text()).toMatch(
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
);
expect(stderr).toMatch(
/Middleware's `config.matcher` .+ Received: not-a-valid-matcher/g
);
} finally {
await dev.kill();
}
});
test('[vercel dev] should support request body', async () => {
const dir = fixture('node-request-body');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const body = { hello: 'world' };
// Test that `req.body` works in dev
let res = await fetch(`http://localhost:${port}/api/req-body`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
validateResponseHeaders(res);
expect(await res.json()).toMatchObject({ body, readBody: body });
// Test that `req` "data" events work in dev
res = await fetch(`http://localhost:${port}/api/data-events`, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
} finally {
await dev.kill();
}
});
test('[vercel dev] should maintain query when invoking serverless function', async () => {
const dir = fixture('node-query-invoke');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const res = await fetch(`http://localhost:${port}/something?url-param=a`);
validateResponseHeaders(res);
const text = await res.text();
const parsed = url.parse(text, true);
expect(parsed.pathname).toEqual('/something');
expect(parsed.query['url-param']).toEqual('a');
expect(parsed.query['route-param']).toEqual('b');
} finally {
await dev.kill();
}
});
test('[vercel dev] should maintain query when proxy passing', async () => {
const dir = fixture('query-proxy');
const { dev, port, readyResolver } = await testFixture(dir);
const dest = createServer((req, res) => {
res.end(req.url);
});
try {
await Promise.all([readyResolver, listen(dest, 0)]);
const destAddr = dest.address();
if (!destAddr || typeof destAddr === 'string') {
throw new Error('Unexpected HTTP address');
}
const res = await fetch(
`http://localhost:${port}/${destAddr.port}?url-param=a`
);
validateResponseHeaders(res);
const text = await res.text();
const parsed = url.parse(text, true);
expect(parsed.pathname).toEqual('/something');
expect(parsed.query['url-param']).toEqual('a');
expect(parsed.query['route-param']).toEqual('b');
} finally {
dest.close();
await dev.kill();
}
});
test('[vercel dev] should maintain query when dev server defines routes', async () => {
const dir = fixture('dev-server-query');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const res = await fetch(`http://localhost:${port}/test?url-param=a`);
validateResponseHeaders(res);
const text = await res.text();
// Hacky way of getting the page payload from the response
// HTML since we don't have a HTML parser handy.
const json = text
.match(/<pre>(.*)<\/pre>/)![1]
.replace('</pre>', '')
.replace('<!-- -->', '')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"');
const parsed = JSON.parse(json);
const query = url.parse(parsed.url, true).query;
expect(query['url-param']).toEqual('a');
expect(query['route-param']).toEqual('b');
} finally {
await dev.kill();
}
});
test('[vercel dev] should allow `cache-control` to be overwritten', async () => {
const dir = fixture('headers');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const res = await fetch(
`http://localhost:${port}/?name=cache-control&value=immutable`
);
expect(res.headers.get('cache-control')).toEqual('immutable');
} finally {
await dev.kill();
}
});
test('[vercel dev] should send `etag` header for static files', async () => {
const dir = fixture('headers');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const res = await fetch(`http://localhost:${port}/foo.txt`);
const expected = 'd263af8ab880c0b97eb6c5c125b5d44f9e5addd9';
expect(res.headers.get('etag')).toEqual(`"${expected}"`);
const body = await res.text();
expect(body.trim()).toEqual('hi');
} finally {
await dev.kill();
}
});
test('[vercel dev] should frontend dev server and routes', async () => {
const dir = fixture('dev-server-and-routes');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let podId: string;
let res = await fetch(`http://localhost:${port}/`);
validateResponseHeaders(res);
podId = res.headers.get('x-vercel-id')!.match(/:(\w+)-/)![1];
let body = await res.text();
expect(body.includes('hello, this is the frontend')).toBeTruthy();
res = await fetch(`http://localhost:${port}/api/users`);
validateResponseHeaders(res, podId);
body = await res.text();
expect(body).toEqual('users');
res = await fetch(`http://localhost:${port}/api/users/1`);
validateResponseHeaders(res, podId);
body = await res.text();
expect(body).toEqual('users/1');
res = await fetch(`http://localhost:${port}/api/welcome`);
validateResponseHeaders(res, podId);
body = await res.text();
expect(body).toEqual('hello and welcome');
} finally {
await dev.kill();
}
});
test('[vercel dev] should support `@vercel/static` routing', async () => {
const dir = fixture('static-routes');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const res = await fetch(`http://localhost:${port}/`);
expect(res.status).toEqual(200);
const body = await res.text();
expect(body.trim()).toEqual('<body>Hello!</body>');
} finally {
await dev.kill();
}
});
test('[vercel dev] should support `@vercel/static-build` routing', async () => {
const dir = fixture('static-build-routing');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const res = await fetch(`http://localhost:${port}/api/date`);
expect(res.status).toEqual(200);
const body = await res.text();
expect(body.startsWith('The current date:')).toBeTruthy();
} finally {
await dev.kill();
}
});
test('[vercel dev] should support directory listing', async () => {
const dir = fixture('directory-listing');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
// Get directory listing
let res = await fetch(`http://localhost:${port}/`);
let body = await res.text();
expect(res.status).toEqual(200);
expect(body.includes('Index of')).toBeTruthy();
// Get a file
res = await fetch(`http://localhost:${port}/file.txt`);
body = await res.text();
expect(res.status).toEqual(200);
expect(body.trim()).toEqual('Hello from file!');
// Invoke a lambda
res = await fetch(`http://localhost:${port}/lambda.js`);
body = await res.text();
expect(res.status).toEqual(200);
expect(body).toEqual('Hello from Lambda!');
// Trigger a 404
res = await fetch(`http://localhost:${port}/does-not-exist`);
expect(res.status).toEqual(404);
} finally {
await dev.kill();
}
});
test('[vercel dev] should respond with 404 listing with Accept header support', async () => {
const dir = fixture('directory-listing');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
// HTML response
let res = await fetch(`http://localhost:${port}/does-not-exist`, {
headers: {
Accept: 'text/html',
},
});
expect(res.status).toEqual(404);
expect(res.headers.get('content-type')).toEqual('text/html; charset=utf-8');
let body = await res.text();
expect(body.startsWith('<!DOCTYPE html>')).toBeTruthy();
// JSON response
res = await fetch(`http://localhost:${port}/does-not-exist`, {
headers: {
Accept: 'application/json',
},
});
expect(res.status).toEqual(404);
expect(res.headers.get('content-type')).toEqual('application/json');
body = await res.text();
expect(body).toEqual(
'{"error":{"code":404,"message":"The page could not be found."}}\n'
);
// Plain text response
res = await fetch(`http://localhost:${port}/does-not-exist`);
expect(res.status).toEqual(404);
body = await res.text();
expect(res.headers.get('content-type')).toEqual(
'text/plain; charset=utf-8'
);
expect(body).toEqual('The page could not be found.\n\nNOT_FOUND\n');
} finally {
await dev.kill();
}
});
test('[vercel dev] should support `public` directory with zero config', async () => {
const dir = fixture('api-with-public');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(`http://localhost:${port}/api/user`);
let body = await res.text();
expect(body).toEqual('hello:user');
res = await fetch(`http://localhost:${port}/`);
body = await res.text();
expect(body.startsWith('<h1>hello world</h1>')).toBeTruthy();
} finally {
await dev.kill();
}
});
test('[vercel dev] should support static files with zero config', async () => {
const dir = fixture('api-with-static');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
let res = await fetch(`http://localhost:${port}/api/user`);
let body = await res.text();
expect(body).toEqual('bye:user');
res = await fetch(`http://localhost:${port}/`);
expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8');
body = await res.text();
expect(body.startsWith('<h1>goodbye world</h1>')).toBeTruthy();
} finally {
await dev.kill();
}
});
test('[vercel dev] should support custom 404 routes', async () => {
const dir = fixture('custom-404');
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
// Test custom 404 with static dest
let res = await fetch(`http://localhost:${port}/error.html`);
expect(res.status).toEqual(404);
let body = await res.text();
expect(body.trim()).toEqual('<div>Custom 404 page</div>');
// Test custom 404 with lambda dest
res = await fetch(`http://localhost:${port}/error.js`);
expect(res.status).toEqual(404);
body = await res.text();
expect(body).toEqual('Custom 404 Lambda\n');
// Test regular 404 still works
res = await fetch(`http://localhost:${port}/does-not-exist`);
expect(res.status).toEqual(404);
body = await res.text();
expect(body).toEqual('The page could not be found.\n\nNOT_FOUND\n');
} finally {
await dev.kill();
}
});
test('[vercel dev] prints `npm install` errors', async () => {
const dir = fixture('runtime-not-installed');
const result = await exec(dir);
expect(
stripAnsi(result.stderr).includes(
'Error: The package `@vercel/does-not-exist` is not published on the npm registry'
)
).toBeTruthy();
expect(
result.stderr.includes(
'https://vercel.link/builder-dependencies-install-failed'
)
).toBeTruthy();
});
test('[vercel dev] `vercel.json` should be invalidated if deleted', async () => {
const dir = fixture('invalidate-vercel-config');
const configPath = join(dir, 'vercel.json');
const originalConfig = await fs.readJSON(configPath);
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
{
// Env var should be set from `vercel.json`
const res = await fetch(`http://localhost:${port}/api`);
const body = await res.json();
expect(body.FOO).toBe('bar');
}
{
// Env var should not be set after `vercel.json` is deleted
await fs.remove(configPath);
const res = await fetch(`http://localhost:${port}/api`);
const body = await res.json();
expect(body.FOO).toBe(undefined);
}
} finally {
await dev.kill();
await fs.writeJSON(configPath, originalConfig);
}
});
test('[vercel dev] reflects changes to config and env without restart', async () => {
const dir = fixture('node-helpers');
const configPath = join(dir, 'vercel.json');
const originalConfig = await fs.readJSON(configPath);
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
{
// Node.js helpers should be available by default
const res = await fetch(`http://localhost:${port}/?foo=bar`);
const body = await res.json();
expect(body.hasHelpers).toBe(true);
expect(body.query.foo).toBe('bar');
}
{
// Disable the helpers via `config.helpers = false`
const config = {
...originalConfig,
builds: [
{
...originalConfig.builds[0],
config: {
helpers: false,
},
},
],
};
await fs.writeJSON(configPath, config);
const res = await fetch(`http://localhost:${port}/?foo=bar`);
const body = await res.json();
expect(body.hasHelpers).toBe(false);
expect(body.query).toBe(undefined);
}
{
// Enable the helpers via `config.helpers = true`
const config = {
...originalConfig,
builds: [
{
...originalConfig.builds[0],
config: {
helpers: true,
},
},
],
};
await fs.writeJSON(configPath, config);
const res = await fetch(`http://localhost:${port}/?foo=baz`);
const body = await res.json();
expect(body.hasHelpers).toBe(true);
expect(body.query.foo).toBe('baz');
}
{
// Disable the helpers via `NODEJS_HELPERS = '0'`
const config = {
...originalConfig,
build: {
env: {
NODEJS_HELPERS: '0',
},
},
};
await fs.writeJSON(configPath, config);
const res = await fetch(`http://localhost:${port}/?foo=baz`);
const body = await res.json();
expect(body.hasHelpers).toBe(false);
expect(body.query).toBe(undefined);
}
{
// Enable the helpers via `NODEJS_HELPERS = '1'`
const config = {
...originalConfig,
build: {
env: {
NODEJS_HELPERS: '1',
},
},
};
await fs.writeJSON(configPath, config);
const res = await fetch(`http://localhost:${port}/?foo=boo`);
const body = await res.json();
expect(body.hasHelpers).toBe(true);
expect(body.query.foo).toBe('boo');
}
} finally {
await dev.kill();
await fs.writeJSON(configPath, originalConfig);
}
});
test('[vercel dev] `@vercel/node` TypeScript should be resolved by default', async () => {
// The purpose of this test is to test that `@vercel/node` can properly
// resolve the default "typescript" module when the project doesn't include
// its own version. To properly test for this, a fixture needs to be created
// *outside* of the `vercel` repo, since otherwise the root-level
// "node_modules/typescript" is resolved as relative to the project, and
// not relative to `@vercel/node` which is what we are testing for here.
const dir = join(os.tmpdir(), 'vercel-node-typescript-resolve-test');
const apiDir = join(dir, 'api');
await fs.mkdirp(apiDir);
await fs.writeFile(
join(apiDir, 'hello.js'),
'export default (req, res) => { res.end("world"); }'
);
const { dev, port, readyResolver } = await testFixture(dir);
try {
await readyResolver;
const res = await fetch(`http://localhost:${port}/api/hello`);
const body = await res.text();
expect(body).toBe('world');
} finally {
await dev.kill();
await fs.remove(dir);
}
});
test(
'[vercel dev] validate routes that use `check: true`',
testFixtureStdio('routes-check-true', async (testPath: any) => {
await testPath(200, '/blog/post', 'Blog Home');
})
);
test(
'[vercel dev] validate routes that use `check: true` and `status` code',
testFixtureStdio('routes-check-true-status', async (testPath: any) => {
await testPath(403, '/secret');
await testPath(200, '/post', 'This is a post.');
await testPath(200, '/post.html', 'This is a post.');
})
);
test(
'[vercel dev] validate routes that use custom 404 page',
testFixtureStdio('routes-custom-404', async (testPath: any) => {
await testPath(200, '/', 'Home Page');
await testPath(404, '/nothing', 'Custom User 404');
await testPath(404, '/exact', 'Exact Custom 404');
await testPath(200, '/api/hello', 'Hello');
await testPath(404, '/api/nothing', 'Custom User 404');
})
);
test(
'[vercel dev] handles miss after route',
testFixtureStdio('handle-miss-after-route', async (testPath: any) => {
await testPath(200, '/post', 'Blog Post Page', {
test: '1',
override: 'one',
});
})
);
test(
'[vercel dev] handles miss after rewrite',
testFixtureStdio('handle-miss-after-rewrite', async (testPath: any) => {
await testPath(200, '/post', 'Blog Post Page', {
test: '1',
override: 'one',
});
await testPath(200, '/blog/post', 'Blog Post Page', {
test: '1',
override: 'two',
});
await testPath(404, '/blog/about.html', undefined, {
test: '1',
override: 'two',
});
})
);
test(
'[vercel dev] does not display directory listing after 404',
testFixtureStdio('handle-miss-hide-dir-list', async (testPath: any) => {
await testPath(404, '/post');
await testPath(200, '/post/one.html', 'First Post');
})
);
test(
'[vercel dev] should preserve query string even after miss phase',
testFixtureStdio('handle-miss-querystring', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
if (process.env.CI && process.platform === 'darwin') {
console.log('Skipping since GH Actions hangs for some reason');
} else {
await testPath(200, '/echo/first/second', 'a=first,b=second');
await testPath(200, '/functions/echo.js?a=one&b=two', 'a=one,b=two');
}
})
);
test(
'[vercel dev] handles hit after handle: filesystem',
testFixtureStdio('handle-hit-after-fs', async (testPath: any) => {
await testPath(200, '/blog.html', 'Blog Page', { test: '1' });
})
);
test(
'[vercel dev] handles hit after dest',
testFixtureStdio('handle-hit-after-dest', async (testPath: any) => {
await testPath(200, '/post', 'Blog Post', { test: '1', override: 'one' });
})
);
test(
'[vercel dev] handles hit after rewrite',
testFixtureStdio('handle-hit-after-rewrite', async (testPath: any) => {
await testPath(200, '/post', 'Blog Post', { test: '1', override: 'one' });
})
);
test(
'[vercel dev] should serve the public directory and api functions',
testFixtureStdio('public-and-api', async (testPath: any) => {
await testPath(200, '/', 'This is the home page');
await testPath(200, '/about.html', 'This is the about page');
await testPath(200, '/.well-known/humans.txt', 'We come in peace');
await testPath(200, '/api/date', /current date/);
await testPath(200, '/api/rand', /random number/);
await testPath(200, '/api/rand.js', /random number/);
await testPath(404, '/api/api', /NOT_FOUND/m);
await testPath(404, '/nothing', /Custom 404 Page/);
})
);
test(
'[vercel dev] should allow user rewrites for path segment files',
testFixtureStdio('test-zero-config-rewrite', async (testPath: any) => {
await testPath(404, '/');
await testPath(200, '/echo/1', '{"id":"1"}', {
'Access-Control-Allow-Origin': '*',
});
await testPath(200, '/echo/2', '{"id":"2"}', {
'Access-Control-Allow-Headers': '*',
});
})
);
test('[vercel dev] validate builds', async () => {
const directory = fixture('invalid-builds');
const output = await exec(directory);
expect(output.exitCode).toBe(1);
expect(output.stderr).toMatch(
/Invalid vercel\.json - `builds\[0\].src` should be string/m
);
});
test('[vercel dev] validate routes', async () => {
const directory = fixture('invalid-routes');
const output = await exec(directory);
expect(output.exitCode).toBe(1);
expect(output.stderr).toMatch(
/Invalid vercel\.json - `routes\[0\].src` should be string/m
);
});
test('[vercel dev] validate cleanUrls', async () => {
const directory = fixture('invalid-clean-urls');
const output = await exec(directory);
expect(output.exitCode).toBe(1);
expect(output.stderr).toMatch(
/Invalid vercel\.json - `cleanUrls` should be boolean/m
);
});
test('[vercel dev] validate trailingSlash', async () => {
const directory = fixture('invalid-trailing-slash');
const output = await exec(directory);
expect(output.exitCode).toBe(1);
expect(output.stderr).toMatch(
/Invalid vercel\.json - `trailingSlash` should be boolean/m
);
});
test('[vercel dev] validate rewrites', async () => {
const directory = fixture('invalid-rewrites');
const output = await exec(directory);
expect(output.exitCode).toBe(1);
expect(output.stderr).toMatch(
/Invalid vercel\.json - `rewrites\[0\].destination` should be string/m
);
});
test(
'[vercel dev] should correctly proxy to vite dev',
testFixtureStdio(
'vite-dev',
async (testPath: any) => {
const url = '/src/App.vue?vue&type=style&index=0&lang.css';
// The first request should return the HTML template
await testPath(200, url, /<template>/gm);
// The second request should return the HMR JS
await testPath(200, url, /__vite__createHotContext/gm);
// Home page should always return HTML
await testPath(200, '/', /<title>Vite App<\/title>/gm);
},
{ skipDeploy: true }
)
);