mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 04:22:01 +00:00
1649 lines
45 KiB
JavaScript
1649 lines
45 KiB
JavaScript
import ms from 'ms';
|
|
import os from 'os';
|
|
import fs from 'fs-extra';
|
|
import test from 'ava';
|
|
import { isIP } from 'net';
|
|
import { join, resolve, delimiter } from 'path';
|
|
import _execa from 'execa';
|
|
import fetch from 'node-fetch';
|
|
import sleep from 'then-sleep';
|
|
import retry from 'async-retry';
|
|
import { satisfies } from 'semver';
|
|
import { getDistTag } from '../../src/util/get-dist-tag';
|
|
import { version as cliVersion } from '../../package.json';
|
|
import { fetchTokenWithRetry } from '../../../../test/lib/deployment/now-deploy';
|
|
|
|
const isCanary = () => getDistTag(cliVersion) === 'canary';
|
|
|
|
let port = 3000;
|
|
|
|
const binaryPath = resolve(__dirname, `../../scripts/start.js`);
|
|
const fixture = name => join('test', 'dev', 'fixtures', name);
|
|
const fixtureAbsolute = name => join(__dirname, 'fixtures', name);
|
|
|
|
let processCounter = 0;
|
|
const processList = new Map();
|
|
|
|
function execa(...args) {
|
|
const procId = ++processCounter;
|
|
const child = _execa(...args);
|
|
|
|
processList.set(procId, child);
|
|
child.on('exit', () => processList.delete(procId));
|
|
|
|
return child;
|
|
}
|
|
|
|
function fetchWithRetry(url, retries = 3, opts = {}) {
|
|
return retry(
|
|
async () => {
|
|
const res = await fetch(url, opts);
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(
|
|
`Failed to fetch ${url} with status ${res.status}:` +
|
|
`\n\n${text}\n\n`
|
|
);
|
|
}
|
|
|
|
return res;
|
|
},
|
|
{
|
|
retries,
|
|
factor: 1,
|
|
}
|
|
);
|
|
}
|
|
|
|
function createResolver() {
|
|
let resolver;
|
|
const p = new Promise(res => (resolver = res));
|
|
p.resolve = resolver;
|
|
return p;
|
|
}
|
|
|
|
function formatOutput({ stderr, stdout }) {
|
|
return `Received:\n"${stderr}"\n"${stdout}"`;
|
|
}
|
|
|
|
function printOutput(fixture, stdout, stderr) {
|
|
const lines = (
|
|
`\nOutput for "${fixture}"\n` +
|
|
`\n----- stdout -----\n` +
|
|
stdout +
|
|
`\n----- stderr -----\n` +
|
|
stderr
|
|
).split('\n');
|
|
|
|
const getPrefix = nr => {
|
|
return nr === 0 ? '╭' : nr === lines.length - 1 ? '╰' : '│';
|
|
};
|
|
|
|
console.log(
|
|
lines.map((line, index) => ` ${getPrefix(index)} ${line}`).join('\n')
|
|
);
|
|
}
|
|
|
|
function shouldSkip(t, name, versions) {
|
|
if (!satisfies(process.version, versions)) {
|
|
console.log(`Skipping "${name}" because it requires "${versions}".`);
|
|
t.pass();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function validateResponseHeaders(t, res) {
|
|
if (res.status < 500) {
|
|
t.truthy(res.headers.get('x-vercel-id'));
|
|
t.truthy(res.headers.get('cache-control').length > 0);
|
|
}
|
|
}
|
|
|
|
async function exec(directory, args = []) {
|
|
return execa(binaryPath, ['dev', directory, ...args], {
|
|
reject: false,
|
|
shell: true,
|
|
env: { __VERCEL_SKIP_DEV_CMD: 1 },
|
|
});
|
|
}
|
|
|
|
async function runNpmInstall(fixturePath) {
|
|
if (await fs.exists(join(fixturePath, 'package.json'))) {
|
|
await execa('yarn', ['install'], {
|
|
cwd: fixturePath,
|
|
shell: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function testPath(
|
|
t,
|
|
isDev,
|
|
origin,
|
|
status,
|
|
path,
|
|
expectedText,
|
|
headers = {},
|
|
method = 'GET'
|
|
) {
|
|
const opts = { redirect: 'manual-dont-change', method };
|
|
const url = `${origin}${path}`;
|
|
const res = await fetch(url, opts);
|
|
const msg = `Testing response from ${method} ${url}`;
|
|
console.log(msg);
|
|
t.is(res.status, status, msg);
|
|
validateResponseHeaders(t, res);
|
|
if (typeof expectedText === 'string') {
|
|
const actualText = await res.text();
|
|
t.is(actualText.trim(), expectedText.trim(), msg);
|
|
} else if (typeof expectedText === 'function') {
|
|
const actualText = await res.text();
|
|
await expectedText(t, actualText, res, isDev);
|
|
} else if (expectedText instanceof RegExp) {
|
|
const actualText = await res.text();
|
|
expectedText.lastIndex = 0; // reset since we test twice
|
|
t.regex(actualText, expectedText);
|
|
}
|
|
if (headers) {
|
|
Object.entries(headers).forEach(([key, expectedValue]) => {
|
|
let actualValue = res.headers.get(key);
|
|
if (key.toLowerCase() === 'location' && actualValue === '//') {
|
|
// HACK: `node-fetch` has strange behavior for location header so fix it
|
|
// with `manual-dont-change` opt and convert double slash to single.
|
|
// See https://github.com/node-fetch/node-fetch/issues/417#issuecomment-587233352
|
|
actualValue = '/';
|
|
}
|
|
t.is(actualValue, expectedValue, msg);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function testFixture(directory, opts = {}, args = []) {
|
|
await runNpmInstall(directory);
|
|
|
|
const dev = execa(
|
|
binaryPath,
|
|
['dev', directory, '-l', String(port), ...args],
|
|
{
|
|
reject: false,
|
|
detached: true,
|
|
shell: true,
|
|
stdio: 'pipe',
|
|
...opts,
|
|
env: { ...opts.env, __VERCEL_SKIP_DEV_CMD: 1 },
|
|
}
|
|
);
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
const readyResolver = createResolver();
|
|
const exitResolver = createResolver();
|
|
|
|
dev.stdout.setEncoding('utf8');
|
|
dev.stderr.setEncoding('utf8');
|
|
|
|
dev.stdout.on('data', data => {
|
|
stdout += data;
|
|
});
|
|
dev.stderr.on('data', data => {
|
|
stderr += data;
|
|
|
|
if (stderr.includes('Ready! Available at')) {
|
|
readyResolver.resolve();
|
|
}
|
|
});
|
|
|
|
let printedOutput = false;
|
|
|
|
dev.on('exit', () => {
|
|
if (!printedOutput) {
|
|
printOutput(directory, stdout, stderr);
|
|
printedOutput = true;
|
|
}
|
|
exitResolver.resolve();
|
|
});
|
|
|
|
dev.on('error', () => {
|
|
if (!printedOutput) {
|
|
printOutput(directory, stdout, stderr);
|
|
printedOutput = true;
|
|
}
|
|
exitResolver.resolve();
|
|
});
|
|
|
|
dev._kill = dev.kill;
|
|
dev.kill = async (...args) => {
|
|
dev._kill(...args);
|
|
await exitResolver;
|
|
};
|
|
|
|
return {
|
|
dev,
|
|
port,
|
|
readyResolver,
|
|
};
|
|
}
|
|
|
|
function testFixtureStdio(
|
|
directory,
|
|
fn,
|
|
{ expectedCode = 0, skipDeploy } = {}
|
|
) {
|
|
return async t => {
|
|
const cwd = fixtureAbsolute(directory);
|
|
const token = await fetchTokenWithRetry();
|
|
let deploymentUrl;
|
|
|
|
// Deploy fixture and link project
|
|
if (!skipDeploy) {
|
|
const project = join(cwd, '.vercel', 'project.json');
|
|
if (await fs.exists(project)) {
|
|
await fs.unlink(project);
|
|
}
|
|
const gitignore = join(cwd, '.gitignore');
|
|
const gitignoreOrig = await fs.exists(gitignore);
|
|
let { stdout, stderr, exitCode } = await execa(
|
|
binaryPath,
|
|
['-t', token, '--confirm', '--public', '--no-clipboard', '--debug'],
|
|
{ cwd, reject: false }
|
|
);
|
|
console.log({ stdout, stderr, exitCode });
|
|
if (!gitignoreOrig && (await fs.exists(gitignore))) {
|
|
await fs.unlink(gitignore);
|
|
}
|
|
t.is(exitCode, expectedCode);
|
|
if (expectedCode === 0) {
|
|
deploymentUrl = new URL(stdout).host;
|
|
}
|
|
}
|
|
|
|
// Start dev
|
|
let dev;
|
|
|
|
await runNpmInstall(cwd);
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
const readyResolver = createResolver();
|
|
const exitResolver = createResolver();
|
|
|
|
try {
|
|
let printedOutput = false;
|
|
|
|
const env = skipDeploy
|
|
? { ...process.env, __VERCEL_SKIP_DEV_CMD: 1 }
|
|
: process.env;
|
|
dev = execa(binaryPath, ['dev', '-l', port, '-t', token, '--debug'], {
|
|
cwd,
|
|
env,
|
|
});
|
|
|
|
dev.stdout.setEncoding('utf8');
|
|
dev.stderr.setEncoding('utf8');
|
|
|
|
dev.stdout.pipe(process.stdout);
|
|
dev.stderr.pipe(process.stderr);
|
|
|
|
dev.stdout.on('data', data => {
|
|
stdout += data;
|
|
});
|
|
|
|
dev.stderr.on('data', data => {
|
|
stderr += data;
|
|
|
|
if (stderr.includes('Ready! Available at')) {
|
|
readyResolver.resolve();
|
|
}
|
|
|
|
if (stderr.includes(`Requested port ${port} is already in use`)) {
|
|
dev.kill('SIGTERM');
|
|
throw new Error(
|
|
`Failed for "${directory}" with port ${port} with stderr "${stderr}".`
|
|
);
|
|
}
|
|
|
|
if (stderr.includes('Command failed') || stderr.includes('Error!')) {
|
|
dev.kill('SIGTERM');
|
|
throw new Error(`Failed for "${directory}" with stderr "${stderr}".`);
|
|
}
|
|
});
|
|
|
|
dev.on('exit', () => {
|
|
if (!printedOutput) {
|
|
printOutput(directory, stdout, stderr);
|
|
printedOutput = true;
|
|
}
|
|
exitResolver.resolve();
|
|
});
|
|
|
|
dev.on('error', () => {
|
|
if (!printedOutput) {
|
|
printOutput(directory, stdout, stderr);
|
|
printedOutput = true;
|
|
}
|
|
exitResolver.resolve();
|
|
});
|
|
|
|
await readyResolver;
|
|
|
|
const helperTestPath = async (...args) => {
|
|
if (!skipDeploy) {
|
|
await testPath(t, false, `https://${deploymentUrl}`, ...args);
|
|
}
|
|
await testPath(t, true, `http://localhost:${port}`, ...args);
|
|
};
|
|
await fn(helperTestPath, t, port);
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
await exitResolver;
|
|
}
|
|
};
|
|
}
|
|
|
|
test.beforeEach(() => {
|
|
port = ++port;
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
await Promise.all(
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
Array.from(processList).map(([_procId, proc]) => {
|
|
if (proc.killed === false) {
|
|
console.log(
|
|
`killing process ${proc.pid} "${proc.spawnargs.join(' ')}"`
|
|
);
|
|
|
|
try {
|
|
process.kill(proc.pid, 'SIGTERM');
|
|
} catch (err) {
|
|
// Was already killed
|
|
if (err.errno !== 'ESRCH') {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] prints `npm install` errors', async t => {
|
|
const dir = fixture('runtime-not-installed');
|
|
const result = await exec(dir);
|
|
t.truthy(result.stderr.includes('npm ERR! 404'));
|
|
t.truthy(
|
|
result.stderr.includes('Failed to install `vercel dev` dependencies')
|
|
);
|
|
t.truthy(
|
|
result.stderr.includes('https://vercel.link/npm-install-failed-dev')
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] `vercel.json` should be invalidated if deleted', async t => {
|
|
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();
|
|
t.is(body.FOO, '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();
|
|
t.is(body.FOO, undefined);
|
|
}
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
await fs.writeJSON(configPath, originalConfig);
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] reflects changes to config and env without restart', async t => {
|
|
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();
|
|
t.is(body.hasHelpers, true);
|
|
t.is(body.query.foo, '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();
|
|
t.is(body.hasHelpers, false);
|
|
t.is(body.query, 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();
|
|
t.is(body.hasHelpers, true);
|
|
t.is(body.query.foo, '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();
|
|
t.is(body.hasHelpers, false);
|
|
t.is(body.query, 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();
|
|
t.is(body.hasHelpers, true);
|
|
t.is(body.query.foo, 'boo');
|
|
}
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
await fs.writeJSON(configPath, originalConfig);
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] `@vercel/node` TypeScript should be resolved by default', async t => {
|
|
// 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();
|
|
t.is(body, 'world');
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
await fs.remove(dir);
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] validate routes that use `check: true`',
|
|
testFixtureStdio('routes-check-true', async testPath => {
|
|
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 => {
|
|
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 => {
|
|
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 => {
|
|
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 => {
|
|
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] displays directory listing after miss',
|
|
testFixtureStdio('handle-miss-display-dir-list', async (testPath) => {
|
|
await testPath(404, '/post', /one.html/m);
|
|
})
|
|
);
|
|
*/
|
|
|
|
test(
|
|
'[vercel dev] does not display directory listing after 404',
|
|
testFixtureStdio('handle-miss-hide-dir-list', async testPath => {
|
|
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 => {
|
|
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 => {
|
|
await testPath(200, '/blog.html', 'Blog Page', { test: '1' });
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] handles hit after dest',
|
|
testFixtureStdio('handle-hit-after-dest', async testPath => {
|
|
await testPath(200, '/post', 'Blog Post', { test: '1', override: 'one' });
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] handles hit after rewrite',
|
|
testFixtureStdio('handle-hit-after-rewrite', async testPath => {
|
|
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 => {
|
|
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 => {
|
|
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 t => {
|
|
const directory = fixture('invalid-builds');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid vercel\.json - `builds\[0\].src` should be string/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate routes', async t => {
|
|
const directory = fixture('invalid-routes');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid vercel\.json - `routes\[0\].src` should be string/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate cleanUrls', async t => {
|
|
const directory = fixture('invalid-clean-urls');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid vercel\.json - `cleanUrls` should be boolean/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate trailingSlash', async t => {
|
|
const directory = fixture('invalid-trailing-slash');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid vercel\.json - `trailingSlash` should be boolean/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate rewrites', async t => {
|
|
const directory = fixture('invalid-rewrites');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid vercel\.json - `rewrites\[0\].destination` should be string/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate redirects', async t => {
|
|
const directory = fixture('invalid-redirects');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid vercel\.json - `redirects\[0\].statusCode` should be integer/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate headers', async t => {
|
|
const directory = fixture('invalid-headers');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/Invalid vercel\.json - `headers\[0\].headers\[0\].value` should be string/m
|
|
);
|
|
});
|
|
|
|
test('[vercel dev] validate mixed routes and rewrites', async t => {
|
|
const directory = fixture('invalid-mixed-routes-rewrites');
|
|
const output = await exec(directory);
|
|
|
|
t.is(output.exitCode, 1, formatOutput(output));
|
|
t.regex(
|
|
output.stderr,
|
|
/If `rewrites`, `redirects`, `headers`, `cleanUrls` or `trailingSlash` are used, then `routes` cannot be present./m
|
|
);
|
|
t.regex(output.stderr, /vercel\.link\/mix-routing-props/m);
|
|
});
|
|
|
|
// Test seems unstable: It won't return sometimes.
|
|
test('[vercel dev] validate env var names', async t => {
|
|
const directory = fixture('invalid-env-var-name');
|
|
const { dev } = await testFixture(directory, { stdio: 'pipe' });
|
|
|
|
try {
|
|
let stderr = '';
|
|
dev.stderr.setEncoding('utf8');
|
|
|
|
await new Promise((resolve, reject) => {
|
|
dev.stderr.on('data', b => {
|
|
stderr += b.toString();
|
|
|
|
if (
|
|
stderr.includes('Ignoring env var "1" because name is invalid') &&
|
|
stderr.includes(
|
|
'Ignoring build env var "_a" because name is invalid'
|
|
) &&
|
|
stderr.includes(
|
|
'Env var names must start with letters, and can only contain alphanumeric characters and underscores'
|
|
)
|
|
) {
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
dev.on('error', reject);
|
|
dev.on('exit', resolve);
|
|
});
|
|
|
|
t.pass();
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
|
|
t.pass();
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] test rewrites with segments serve correct content',
|
|
testFixtureStdio('test-rewrites-with-segments', async testPath => {
|
|
await testPath(200, '/api/users/first', 'first');
|
|
await testPath(200, '/api/fourty-two', '42');
|
|
await testPath(200, '/rand', '42');
|
|
await testPath(200, '/api/dynamic', 'dynamic');
|
|
await testPath(404, '/api');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] test rewrites serve correct content',
|
|
testFixtureStdio('test-rewrites', async testPath => {
|
|
await testPath(200, '/hello', 'Hello World');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] test rewrites and redirects is case sensitive',
|
|
testFixtureStdio('test-routing-case-sensitive', async testPath => {
|
|
await testPath(200, '/Path', 'UPPERCASE');
|
|
await testPath(200, '/path', 'lowercase');
|
|
await testPath(308, '/GoTo', 'Redirecting to /upper.html (308)', {
|
|
Location: '/upper.html',
|
|
});
|
|
await testPath(308, '/goto', 'Redirecting to /lower.html (308)', {
|
|
Location: '/lower.html',
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] test cleanUrls serve correct content',
|
|
testFixtureStdio('test-clean-urls', async testPath => {
|
|
await testPath(200, '/', 'Index Page');
|
|
await testPath(200, '/about', 'About Page');
|
|
await testPath(200, '/sub', 'Sub Index Page');
|
|
await testPath(200, '/sub/another', 'Sub Another Page');
|
|
await testPath(200, '/style.css', 'body { color: green }');
|
|
await testPath(308, '/index.html', 'Redirecting to / (308)', {
|
|
Location: '/',
|
|
});
|
|
await testPath(308, '/about.html', 'Redirecting to /about (308)', {
|
|
Location: '/about',
|
|
});
|
|
await testPath(308, '/sub/index.html', 'Redirecting to /sub (308)', {
|
|
Location: '/sub',
|
|
});
|
|
await testPath(
|
|
308,
|
|
'/sub/another.html',
|
|
'Redirecting to /sub/another (308)',
|
|
{ Location: '/sub/another' }
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] should serve custom 404 when `cleanUrls: true`',
|
|
testFixtureStdio('test-clean-urls-custom-404', async testPath => {
|
|
await testPath(200, '/', 'This is the home page');
|
|
await testPath(200, '/about', 'The about page');
|
|
await testPath(200, '/contact/me', 'Contact Me Subdirectory');
|
|
await testPath(404, '/nothing', 'Custom 404 Page');
|
|
await testPath(404, '/nothing/', 'Custom 404 Page');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] test cleanUrls and trailingSlash serve correct content',
|
|
testFixtureStdio('test-clean-urls-trailing-slash', async testPath => {
|
|
await testPath(200, '/', 'Index Page');
|
|
await testPath(200, '/about/', 'About Page');
|
|
await testPath(200, '/sub/', 'Sub Index Page');
|
|
await testPath(200, '/sub/another/', 'Sub Another Page');
|
|
await testPath(200, '/style.css', 'body { color: green }');
|
|
//TODO: fix this test so that location is `/` instead of `//`
|
|
//await testPath(308, '/index.html', 'Redirecting to / (308)', { Location: '/' });
|
|
await testPath(308, '/about.html', 'Redirecting to /about/ (308)', {
|
|
Location: '/about/',
|
|
});
|
|
await testPath(308, '/sub/index.html', 'Redirecting to /sub/ (308)', {
|
|
Location: '/sub/',
|
|
});
|
|
await testPath(
|
|
308,
|
|
'/sub/another.html',
|
|
'Redirecting to /sub/another/ (308)',
|
|
{
|
|
Location: '/sub/another/',
|
|
}
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] test cors headers work with OPTIONS',
|
|
testFixtureStdio('test-cors-routes', async testPath => {
|
|
const headers = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers':
|
|
'Content-Type, Authorization, Accept, Content-Length, Origin, User-Agent',
|
|
'Access-Control-Allow-Methods':
|
|
'GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE',
|
|
};
|
|
await testPath(200, '/', 'status api', headers, 'GET');
|
|
await testPath(200, '/', 'status api', headers, 'POST');
|
|
await testPath(200, '/api/status.js', 'status api', headers, 'GET');
|
|
await testPath(200, '/api/status.js', 'status api', headers, 'POST');
|
|
await testPath(204, '/', '', headers, 'OPTIONS');
|
|
await testPath(204, '/api/status.js', '', headers, 'OPTIONS');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] test trailingSlash true serve correct content',
|
|
testFixtureStdio('test-trailing-slash', async testPath => {
|
|
await testPath(200, '/', 'Index Page');
|
|
await testPath(200, '/index.html', 'Index Page');
|
|
await testPath(200, '/about.html', 'About Page');
|
|
await testPath(200, '/sub/', 'Sub Index Page');
|
|
await testPath(200, '/sub/index.html', 'Sub Index Page');
|
|
await testPath(200, '/sub/another.html', 'Sub Another Page');
|
|
await testPath(200, '/style.css', 'body { color: green }');
|
|
await testPath(308, '/about.html/', 'Redirecting to /about.html (308)', {
|
|
Location: '/about.html',
|
|
});
|
|
await testPath(308, '/style.css/', 'Redirecting to /style.css (308)', {
|
|
Location: '/style.css',
|
|
});
|
|
await testPath(308, '/sub', 'Redirecting to /sub/ (308)', {
|
|
Location: '/sub/',
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] should serve custom 404 when `trailingSlash: true`',
|
|
testFixtureStdio('test-trailing-slash-custom-404', async testPath => {
|
|
await testPath(200, '/', 'This is the home page');
|
|
await testPath(200, '/about.html', 'The about page');
|
|
await testPath(200, '/contact/', 'Contact Subdirectory');
|
|
await testPath(404, '/nothing/', 'Custom 404 Page');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] test trailingSlash false serve correct content',
|
|
testFixtureStdio('test-trailing-slash-false', async testPath => {
|
|
await testPath(200, '/', 'Index Page');
|
|
await testPath(200, '/index.html', 'Index Page');
|
|
await testPath(200, '/about.html', 'About Page');
|
|
await testPath(200, '/sub', 'Sub Index Page');
|
|
await testPath(200, '/sub/index.html', 'Sub Index Page');
|
|
await testPath(200, '/sub/another.html', 'Sub Another Page');
|
|
await testPath(200, '/style.css', 'body { color: green }');
|
|
await testPath(308, '/about.html/', 'Redirecting to /about.html (308)', {
|
|
Location: '/about.html',
|
|
});
|
|
await testPath(308, '/sub/', 'Redirecting to /sub (308)', {
|
|
Location: '/sub',
|
|
});
|
|
await testPath(
|
|
308,
|
|
'/sub/another.html/',
|
|
'Redirecting to /sub/another.html (308)',
|
|
{
|
|
Location: '/sub/another.html',
|
|
}
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] throw when invalid builder routes detected',
|
|
testFixtureStdio(
|
|
'invalid-builder-routes',
|
|
async testPath => {
|
|
await testPath(
|
|
500,
|
|
'/',
|
|
/Route at index 0 has invalid `src` regular expression/m
|
|
);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] support legacy `@now` scope runtimes',
|
|
testFixtureStdio('legacy-now-runtime', async testPath => {
|
|
await testPath(200, '/', /A simple deployment with the Vercel API!/m);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] support dynamic next.js routes in monorepos',
|
|
testFixtureStdio('monorepo-dynamic-paths', async testPath => {
|
|
await testPath(200, '/', /This is our homepage/m);
|
|
await testPath(200, '/about', /This is the about static page./m);
|
|
await testPath(
|
|
200,
|
|
'/1/dynamic',
|
|
/This is the (.*)dynamic(.*) page with static props./m
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 00-list-directory',
|
|
testFixtureStdio('00-list-directory', async testPath => {
|
|
await testPath(200, '/', /Files within/m);
|
|
await testPath(200, '/', /test[0-3]\.txt/m);
|
|
await testPath(200, '/', /\.well-known/m);
|
|
await testPath(200, '/.well-known/keybase.txt', 'proof goes here');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 01-node',
|
|
testFixtureStdio('01-node', async testPath => {
|
|
await testPath(200, '/', /A simple deployment with the Vercel API!/m);
|
|
})
|
|
);
|
|
|
|
// Angular has `engines: { node: "10.x" }` in its `package.json`
|
|
test('[vercel dev] 02-angular-node', async t => {
|
|
if (shouldSkip(t, '02-angular-node', '10.x')) return;
|
|
|
|
const directory = fixture('02-angular-node');
|
|
const { dev, port } = await testFixture(directory, { stdio: 'pipe' }, [
|
|
'--debug',
|
|
]);
|
|
|
|
let stderr = '';
|
|
|
|
try {
|
|
dev.stderr.on('data', async data => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
// start `vercel dev` detached in child_process
|
|
dev.unref();
|
|
|
|
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
|
|
|
|
validateResponseHeaders(t, response);
|
|
|
|
const body = await response.text();
|
|
t.regex(body, /Angular \+ Node.js API/m);
|
|
} finally {
|
|
dev.kill('SIGTERM');
|
|
}
|
|
|
|
await sleep(5000);
|
|
|
|
if (isCanary()) {
|
|
stderr.includes('@now/build-utils@canary');
|
|
} else {
|
|
stderr.includes('@now/build-utils@latest');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] 03-aurelia',
|
|
testFixtureStdio(
|
|
'03-aurelia',
|
|
async testPath => {
|
|
await testPath(200, '/', /Aurelia Navigation Skeleton/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 04-create-react-app',
|
|
testFixtureStdio('04-create-react-app', async testPath => {
|
|
await testPath(200, '/', /React App/m);
|
|
})
|
|
);
|
|
/*
|
|
test(
|
|
'[vercel dev] 05-gatsby',
|
|
testFixtureStdio('05-gatsby', async testPath => {
|
|
await testPath(200, '/', /Gatsby Default Starter/m);
|
|
})
|
|
);
|
|
*/
|
|
test(
|
|
'[vercel dev] 06-gridsome',
|
|
testFixtureStdio('06-gridsome', async testPath => {
|
|
await testPath(200, '/');
|
|
await testPath(200, '/about');
|
|
await testPath(308, '/support', 'Redirecting to /about?ref=support (308)', {
|
|
Location: '/about?ref=support',
|
|
});
|
|
// Bug with gridsome's dev server: https://github.com/gridsome/gridsome/issues/831
|
|
// Works in prod only so leave out for now
|
|
// await testPath(404, '/nothing');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 07-hexo-node',
|
|
testFixtureStdio('07-hexo-node', async testPath => {
|
|
await testPath(200, '/', /Hexo \+ Node.js API/m);
|
|
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
|
await testPath(200, '/contact.html', /Contact Us/m);
|
|
await testPath(200, '/support', /Contact Us/m);
|
|
})
|
|
);
|
|
|
|
test('[vercel dev] 08-hugo', async t => {
|
|
if (process.platform === 'darwin') {
|
|
// Update PATH to find the Hugo executable installed via GH Actions
|
|
process.env.PATH = `${resolve(fixture('08-hugo'))}${delimiter}${
|
|
process.env.PATH
|
|
}`;
|
|
const tester = testFixtureStdio('08-hugo', async testPath => {
|
|
await testPath(200, '/', /Hugo/m);
|
|
});
|
|
await tester(t);
|
|
} else {
|
|
console.log(`Skipping 08-hugo on platform ${process.platform}`);
|
|
t.pass();
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] 10-nextjs-node',
|
|
testFixtureStdio('10-nextjs-node', async testPath => {
|
|
await testPath(200, '/', /Next.js \+ Node.js API/m);
|
|
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
|
await testPath(200, '/contact', /Contact Page/);
|
|
await testPath(200, '/support', /Contact Page/);
|
|
await testPath(404, '/nothing', /Custom Next 404/);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 12-polymer-node',
|
|
testFixtureStdio(
|
|
'12-polymer-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /Polymer \+ Node.js API/m);
|
|
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 13-preact-node',
|
|
testFixtureStdio(
|
|
'13-preact-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /Preact/m);
|
|
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 14-svelte-node',
|
|
testFixtureStdio(
|
|
'14-svelte-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /Svelte/m);
|
|
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 16-vue-node',
|
|
testFixtureStdio(
|
|
'16-vue-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /Vue.js \+ Node.js API/m);
|
|
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 17-vuepress-node',
|
|
testFixtureStdio(
|
|
'17-vuepress-node',
|
|
async testPath => {
|
|
await testPath(200, '/', /VuePress \+ Node.js API/m);
|
|
await testPath(200, '/api/date', new RegExp(new Date().getFullYear()));
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] double slashes redirect',
|
|
testFixtureStdio(
|
|
'01-node',
|
|
async (_testPath, t, port) => {
|
|
{
|
|
const res = await fetch(`http://localhost:${port}////?foo=bar`, {
|
|
redirect: 'manual',
|
|
});
|
|
|
|
validateResponseHeaders(t, res);
|
|
|
|
const body = await res.text();
|
|
t.is(res.status, 301);
|
|
t.is(res.headers.get('location'), `http://localhost:${port}/?foo=bar`);
|
|
t.is(body, 'Redirecting to /?foo=bar (301)\n');
|
|
}
|
|
|
|
{
|
|
const res = await fetch(`http://localhost:${port}///api////date.js`, {
|
|
method: 'POST',
|
|
redirect: 'manual',
|
|
});
|
|
|
|
validateResponseHeaders(t, res);
|
|
|
|
const body = await res.text();
|
|
t.is(res.status, 200);
|
|
t.truthy(
|
|
body.startsWith('January') ||
|
|
body.startsWith('February') ||
|
|
body.startsWith('March') ||
|
|
body.startsWith('April') ||
|
|
body.startsWith('May') ||
|
|
body.startsWith('June') ||
|
|
body.startsWith('July') ||
|
|
body.startsWith('August') ||
|
|
body.startsWith('September') ||
|
|
body.startsWith('October') ||
|
|
body.startsWith('November') ||
|
|
body.startsWith('December')
|
|
);
|
|
}
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 18-marko',
|
|
testFixtureStdio(
|
|
'18-marko',
|
|
async testPath => {
|
|
await testPath(200, '/', /Marko Starter/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 19-mithril',
|
|
testFixtureStdio(
|
|
'19-mithril',
|
|
async testPath => {
|
|
await testPath(200, '/', /Mithril on Vercel/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 20-riot',
|
|
testFixtureStdio(
|
|
'20-riot',
|
|
async testPath => {
|
|
await testPath(200, '/', /Riot on Vercel/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 21-charge',
|
|
testFixtureStdio(
|
|
'21-charge',
|
|
async testPath => {
|
|
await testPath(200, '/', /Welcome to my new Charge site/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 22-brunch',
|
|
testFixtureStdio(
|
|
'22-brunch',
|
|
async testPath => {
|
|
await testPath(200, '/', /Bon Appétit./m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 23-docusaurus',
|
|
testFixtureStdio(
|
|
'23-docusaurus',
|
|
async testPath => {
|
|
await testPath(200, '/', /My Site/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test('[vercel dev] 24-ember', async t => {
|
|
if (shouldSkip(t, '24-ember', '>^6.14.0 || ^8.10.0 || >=9.10.0')) return;
|
|
|
|
const tester = await testFixtureStdio(
|
|
'24-ember',
|
|
async testPath => {
|
|
await testPath(200, '/', /HelloWorld/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
);
|
|
|
|
await tester(t);
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] temporary directory listing',
|
|
testFixtureStdio(
|
|
'temporary-directory-listing',
|
|
async (_testPath, t, port) => {
|
|
const directory = fixture('temporary-directory-listing');
|
|
await fs.unlink(join(directory, 'index.txt')).catch(() => null);
|
|
|
|
await sleep(ms('20s'));
|
|
|
|
const firstResponse = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, firstResponse);
|
|
const body = await firstResponse.text();
|
|
t.is(firstResponse.status, 404, `Received instead: ${body}`);
|
|
|
|
await fs.writeFile(join(directory, 'index.txt'), 'hello');
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, response);
|
|
|
|
if (response.status === 200) {
|
|
const body = await response.text();
|
|
t.is(body, 'hello');
|
|
}
|
|
|
|
await sleep(ms('1s'));
|
|
}
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test('[vercel dev] add a `package.json` to trigger `@vercel/static-build`', async t => {
|
|
const directory = fixture('trigger-static-build');
|
|
|
|
await fs.unlink(join(directory, 'package.json')).catch(() => null);
|
|
|
|
await fs.unlink(join(directory, 'public', 'index.txt')).catch(() => null);
|
|
|
|
await fs.rmdir(join(directory, 'public')).catch(() => null);
|
|
|
|
const tester = testFixtureStdio(
|
|
'trigger-static-build',
|
|
async (_testPath, t, port) => {
|
|
{
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, response);
|
|
const body = await response.text();
|
|
t.is(body.trim(), 'hello:index.txt');
|
|
}
|
|
|
|
const rnd = Math.random().toString();
|
|
const pkg = {
|
|
private: true,
|
|
scripts: { build: `mkdir -p public && echo ${rnd} > public/index.txt` },
|
|
};
|
|
|
|
await fs.writeFile(join(directory, 'package.json'), JSON.stringify(pkg));
|
|
|
|
// Wait until file events have been processed
|
|
await sleep(ms('2s'));
|
|
|
|
{
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, response);
|
|
const body = await response.text();
|
|
t.is(body.trim(), rnd);
|
|
}
|
|
},
|
|
{ skipDeploy: true }
|
|
);
|
|
|
|
await tester(t);
|
|
});
|
|
|
|
test('[vercel dev] no build matches warning', async t => {
|
|
const directory = fixture('no-build-matches');
|
|
const { dev } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
// start `vercel dev` detached in child_process
|
|
dev.unref();
|
|
|
|
dev.stderr.setEncoding('utf8');
|
|
await new Promise(resolve => {
|
|
dev.stderr.on('data', str => {
|
|
if (str.includes('did not match any source files')) {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
t.pass();
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] do not recursivly check the path',
|
|
testFixtureStdio('handle-filesystem-missing', async testPath => {
|
|
await testPath(200, '/', /hello/m);
|
|
await testPath(404, '/favicon.txt');
|
|
})
|
|
);
|
|
|
|
test('[vercel dev] render warning for empty cwd dir', async t => {
|
|
const directory = fixture('empty');
|
|
const { dev, port } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
dev.unref();
|
|
|
|
// Monitor `stderr` for the warning
|
|
dev.stderr.setEncoding('utf8');
|
|
const msg = 'There are no files inside your deployment.';
|
|
await new Promise(resolve => {
|
|
dev.stderr.on('data', str => {
|
|
if (str.includes(msg)) {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Issue a request to ensure a 404 response
|
|
await sleep(ms('3s'));
|
|
const response = await fetch(`http://localhost:${port}`);
|
|
validateResponseHeaders(t, response);
|
|
t.is(response.status, 404);
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('[vercel dev] do not rebuild for changes in the output directory', async t => {
|
|
const directory = fixture('output-is-source');
|
|
|
|
const { dev, port } = await testFixture(directory, {
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
try {
|
|
dev.unref();
|
|
|
|
let stderr = [];
|
|
const start = Date.now();
|
|
|
|
dev.stderr.on('data', str => stderr.push(str));
|
|
|
|
while (stderr.join('').includes('Ready') === false) {
|
|
await sleep(ms('3s'));
|
|
|
|
if (Date.now() - start > ms('30s')) {
|
|
console.log('stderr:', stderr.join(''));
|
|
break;
|
|
}
|
|
}
|
|
|
|
const resp1 = await fetch(`http://localhost:${port}`);
|
|
const text1 = await resp1.text();
|
|
t.is(text1.trim(), 'hello first', stderr.join(''));
|
|
|
|
await fs.writeFile(join(directory, 'public', 'index.html'), 'hello second');
|
|
|
|
await sleep(ms('3s'));
|
|
|
|
const resp2 = await fetch(`http://localhost:${port}`);
|
|
const text2 = await resp2.text();
|
|
t.is(text2.trim(), 'hello second', stderr.join(''));
|
|
} finally {
|
|
await dev.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test(
|
|
'[vercel dev] 25-nextjs-src-dir',
|
|
testFixtureStdio('25-nextjs-src-dir', async testPath => {
|
|
await testPath(200, '/', /Next.js \+ Node.js API/m);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 26-nextjs-secrets',
|
|
testFixtureStdio(
|
|
'26-nextjs-secrets',
|
|
async testPath => {
|
|
await testPath(200, '/api/user', /runtime/m);
|
|
await testPath(200, '/', /buildtime/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 27-zero-config-env',
|
|
testFixtureStdio(
|
|
'27-zero-config-env',
|
|
async testPath => {
|
|
await testPath(200, '/api/print', /build-and-runtime/m);
|
|
await testPath(200, '/', /build-and-runtime/m);
|
|
},
|
|
{ skipDeploy: true }
|
|
)
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] 28-vercel-json-and-ignore',
|
|
testFixtureStdio('28-vercel-json-and-ignore', async testPath => {
|
|
await testPath(200, '/api/one', 'One');
|
|
await testPath(404, '/api/two');
|
|
await testPath(200, '/api/three', 'One');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Use `@vercel/python` with Flask requirements.txt',
|
|
testFixtureStdio('python-flask', async testPath => {
|
|
const name = 'Alice';
|
|
const year = new Date().getFullYear();
|
|
await testPath(200, `/api/user?name=${name}`, new RegExp(`Hello ${name}`));
|
|
await testPath(200, `/api/date`, new RegExp(`Current date is ${year}`));
|
|
await testPath(200, `/api/date.py`, new RegExp(`Current date is ${year}`));
|
|
await testPath(200, `/api/headers`, (t, body, res) => {
|
|
const { host } = new URL(res.url);
|
|
t.is(body, host);
|
|
});
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Use custom runtime from the "functions" property',
|
|
testFixtureStdio('custom-runtime', async testPath => {
|
|
await testPath(200, `/api/user`, /Hello, from Bash!/m);
|
|
await testPath(200, `/api/user.sh`, /Hello, from Bash!/m);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Should work with nested `tsconfig.json` files',
|
|
testFixtureStdio('nested-tsconfig', async testPath => {
|
|
await testPath(200, `/`, /Nested tsconfig.json test page/);
|
|
await testPath(200, `/api`, 'Nested `tsconfig.json` API endpoint');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Should force `tsc` option "module: commonjs" for `startDevServer()`',
|
|
testFixtureStdio('force-module-commonjs', async testPath => {
|
|
await testPath(200, `/`, /Force "module: commonjs" test page/);
|
|
await testPath(
|
|
200,
|
|
`/api`,
|
|
'Force "module: commonjs" JavaScript with ES Modules API endpoint'
|
|
);
|
|
await testPath(
|
|
200,
|
|
`/api/ts`,
|
|
'Force "module: commonjs" TypeScript API endpoint'
|
|
);
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] should prioritize index.html over other file named index.*',
|
|
testFixtureStdio('index-html-priority', async testPath => {
|
|
await testPath(200, '/', 'This is index.html');
|
|
await testPath(200, '/index.css', 'This is index.css');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Should support `*.go` API serverless functions',
|
|
testFixtureStdio('go', async testPath => {
|
|
await testPath(200, `/api`, 'This is the index page');
|
|
await testPath(200, `/api/index`, 'This is the index page');
|
|
await testPath(200, `/api/index.go`, 'This is the index page');
|
|
await testPath(200, `/api/another`, 'This is another page');
|
|
await testPath(200, '/api/another.go', 'This is another page');
|
|
await testPath(200, `/api/foo`, 'Req Path: /api/foo');
|
|
await testPath(200, `/api/bar`, 'Req Path: /api/bar');
|
|
})
|
|
);
|
|
|
|
test(
|
|
'[vercel dev] Should set the `ts-node` "target" to match Node.js version',
|
|
testFixtureStdio('node-ts-node-target', async testPath => {
|
|
await testPath(200, `/api/subclass`, '{"ok":true}');
|
|
await testPath(
|
|
200,
|
|
`/api/array`,
|
|
'{"months":[1,2,3,4,5,6,7,8,9,10,11,12]}'
|
|
);
|
|
|
|
await testPath(200, `/api/dump`, (t, body, res, isDev) => {
|
|
const { host } = new URL(res.url);
|
|
const { env, headers } = JSON.parse(body);
|
|
|
|
// Test that the API endpoint receives the Vercel proxy request headers
|
|
t.is(headers['x-forwarded-host'], host);
|
|
t.is(headers['x-vercel-deployment-url'], host);
|
|
t.truthy(isIP(headers['x-real-ip']));
|
|
t.truthy(isIP(headers['x-forwarded-for']));
|
|
t.truthy(isIP(headers['x-vercel-forwarded-for']));
|
|
|
|
// Test that the API endpoint has the Vercel platform env vars defined.
|
|
t.regex(env.NOW_REGION, /^[a-z]{3}\d$/);
|
|
if (isDev) {
|
|
// Only dev is tested because in production these are opt-in.
|
|
t.is(env.VERCEL_URL, host);
|
|
t.is(env.VERCEL_REGION, 'dev1');
|
|
}
|
|
});
|
|
})
|
|
);
|