mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 21:07:47 +00:00
Compare commits
22 Commits
@vercel/py
...
@vercel/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
621b53bc49 | ||
|
|
728b620355 | ||
|
|
7d16395038 | ||
|
|
59e1259688 | ||
|
|
169242157e | ||
|
|
db10ffd679 | ||
|
|
c0d0744c4e | ||
|
|
9da67423a5 | ||
|
|
51fe09d5e9 | ||
|
|
695bfbdd60 | ||
|
|
547e88228e | ||
|
|
9bfb5dd535 | ||
|
|
81ea84fae8 | ||
|
|
fa8bf07be4 | ||
|
|
cc9dce73ad | ||
|
|
bba7cbd411 | ||
|
|
9a3739bebd | ||
|
|
8c62de16ce | ||
|
|
e9333988d7 | ||
|
|
fb001ce7eb | ||
|
|
b399fe7037 | ||
|
|
88385b3c84 |
@@ -24,7 +24,6 @@ export function sendToVercelAnalytics(metric) {
|
|||||||
speed: getConnectionSpeed(),
|
speed: getConnectionSpeed(),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log({ body });
|
|
||||||
const blob = new Blob([new URLSearchParams(body).toString()], {
|
const blob = new Blob([new URLSearchParams(body).toString()], {
|
||||||
// This content type is necessary for `sendBeacon`
|
// This content type is necessary for `sendBeacon`
|
||||||
type: 'application/x-www-form-urlencoded',
|
type: 'application/x-www-form-urlencoded',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vercel/build-utils",
|
"name": "@vercel/build-utils",
|
||||||
"version": "4.2.0",
|
"version": "5.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.js",
|
"types": "./dist/index.d.js",
|
||||||
@@ -31,7 +31,6 @@
|
|||||||
"@types/node-fetch": "^2.1.6",
|
"@types/node-fetch": "^2.1.6",
|
||||||
"@types/semver": "6.0.0",
|
"@types/semver": "6.0.0",
|
||||||
"@types/yazl": "2.4.2",
|
"@types/yazl": "2.4.2",
|
||||||
"@vercel/frameworks": "1.0.2",
|
|
||||||
"@vercel/ncc": "0.24.0",
|
"@vercel/ncc": "0.24.0",
|
||||||
"aggregate-error": "3.0.1",
|
"aggregate-error": "3.0.1",
|
||||||
"async-retry": "1.2.3",
|
"async-retry": "1.2.3",
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ export interface SpawnOptionsExtended extends SpawnOptions {
|
|||||||
* Pretty formatted command that is being spawned for logging purposes.
|
* Pretty formatted command that is being spawned for logging purposes.
|
||||||
*/
|
*/
|
||||||
prettyCommand?: string;
|
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(
|
export function spawnAsync(
|
||||||
@@ -79,7 +86,7 @@ export function spawnAsync(
|
|||||||
|
|
||||||
child.on('error', reject);
|
child.on('error', reject);
|
||||||
child.on('close', (code, signal) => {
|
child.on('close', (code, signal) => {
|
||||||
if (code === 0) {
|
if (code === 0 || opts.ignoreNon0Exit) {
|
||||||
return resolve();
|
return resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,24 +130,24 @@ export function execAsync(
|
|||||||
|
|
||||||
child.on('error', reject);
|
child.on('error', reject);
|
||||||
child.on('close', (code, signal) => {
|
child.on('close', (code, signal) => {
|
||||||
if (code !== 0) {
|
if (code === 0 || opts.ignoreNon0Exit) {
|
||||||
const cmd = opts.prettyCommand
|
return resolve({
|
||||||
? `Command "${opts.prettyCommand}"`
|
code,
|
||||||
: 'Command';
|
stdout: Buffer.concat(stdoutList).toString(),
|
||||||
|
stderr: Buffer.concat(stderrList).toString(),
|
||||||
return reject(
|
});
|
||||||
new NowBuildError({
|
|
||||||
code: `BUILD_UTILS_EXEC_${code || signal}`,
|
|
||||||
message: `${cmd} exited with ${code || signal}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolve({
|
const cmd = opts.prettyCommand
|
||||||
code,
|
? `Command "${opts.prettyCommand}"`
|
||||||
stdout: Buffer.concat(stdoutList).toString(),
|
: 'Command';
|
||||||
stderr: Buffer.concat(stderrList).toString(),
|
|
||||||
});
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodeBinPath({ cwd }: { cwd: string }) {
|
export async function getNodeBinPath({
|
||||||
const { stdout } = await execAsync('npm', ['bin'], { cwd });
|
cwd,
|
||||||
return stdout.trim();
|
}: {
|
||||||
|
cwd: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
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) {
|
async function chmodPlusX(fsPath: string) {
|
||||||
@@ -205,10 +233,23 @@ export function getSpawnOptions(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!meta.isDev) {
|
if (!meta.isDev) {
|
||||||
// Ensure that the selected Node version is at the beginning of the `$PATH`
|
let found = false;
|
||||||
opts.env.PATH = `/node${nodeVersion.major}/bin${path.delimiter}${
|
const oldPath = opts.env.PATH || process.env.PATH || '';
|
||||||
opts.env.PATH || process.env.PATH
|
|
||||||
}`;
|
const pathSegments = oldPath.split(path.delimiter).map(segment => {
|
||||||
|
if (/^\/node[0-9]+\/bin/.test(segment)) {
|
||||||
|
found = true;
|
||||||
|
return `/node${nodeVersion.major}/bin`;
|
||||||
|
}
|
||||||
|
return segment;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
// If we didn't find & replace, prepend at beginning of PATH
|
||||||
|
pathSegments.unshift(`/node${nodeVersion.major}/bin`);
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.env.PATH = pathSegments.filter(Boolean).join(path.delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
@@ -446,20 +487,31 @@ export function getEnvForPackageManager({
|
|||||||
env: { [x: string]: string | undefined };
|
env: { [x: string]: string | undefined };
|
||||||
}) {
|
}) {
|
||||||
const newEnv: { [x: string]: string | undefined } = { ...env };
|
const newEnv: { [x: string]: string | undefined } = { ...env };
|
||||||
|
const oldPath = env.PATH + '';
|
||||||
|
const npm7 = '/node16/bin-npm7';
|
||||||
|
const pnpm7 = '/pnpm7/node_modules/.bin';
|
||||||
|
const corepackEnabled = env.ENABLE_EXPERIMENTAL_COREPACK === '1';
|
||||||
if (cliType === 'npm') {
|
if (cliType === 'npm') {
|
||||||
if (
|
if (
|
||||||
typeof lockfileVersion === 'number' &&
|
typeof lockfileVersion === 'number' &&
|
||||||
lockfileVersion >= 2 &&
|
lockfileVersion >= 2 &&
|
||||||
(nodeVersion?.major || 0) < 16
|
(nodeVersion?.major || 0) < 16 &&
|
||||||
|
!oldPath.includes(npm7) &&
|
||||||
|
!corepackEnabled
|
||||||
) {
|
) {
|
||||||
// Ensure that npm 7 is at the beginning of the `$PATH`
|
// Ensure that npm 7 is at the beginning of the `$PATH`
|
||||||
newEnv.PATH = `/node16/bin-npm7${path.delimiter}${env.PATH}`;
|
newEnv.PATH = `${npm7}${path.delimiter}${oldPath}`;
|
||||||
console.log('Detected `package-lock.json` generated by npm 7...');
|
console.log('Detected `package-lock.json` generated by npm 7+...');
|
||||||
}
|
}
|
||||||
} else if (cliType === 'pnpm') {
|
} else if (cliType === 'pnpm') {
|
||||||
if (typeof lockfileVersion === 'number' && lockfileVersion === 5.4) {
|
if (
|
||||||
|
typeof lockfileVersion === 'number' &&
|
||||||
|
lockfileVersion === 5.4 &&
|
||||||
|
!oldPath.includes(pnpm7) &&
|
||||||
|
!corepackEnabled
|
||||||
|
) {
|
||||||
// Ensure that pnpm 7 is at the beginning of the `$PATH`
|
// Ensure that pnpm 7 is at the beginning of the `$PATH`
|
||||||
newEnv.PATH = `/pnpm7/node_modules/.bin${path.delimiter}${env.PATH}`;
|
newEnv.PATH = `${pnpm7}${path.delimiter}${oldPath}`;
|
||||||
console.log('Detected `pnpm-lock.yaml` generated by pnpm 7...');
|
console.log('Detected `pnpm-lock.yaml` generated by pnpm 7...');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -80,16 +80,6 @@ export {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { EdgeFunction } from './edge-function';
|
export { EdgeFunction } from './edge-function';
|
||||||
export {
|
|
||||||
detectBuilders,
|
|
||||||
detectOutputDirectory,
|
|
||||||
detectApiDirectory,
|
|
||||||
detectApiExtensions,
|
|
||||||
} from './detect-builders';
|
|
||||||
export { detectFileSystemAPI } from './detect-file-system-api';
|
|
||||||
export { detectFramework } from './detect-framework';
|
|
||||||
export { getProjectPaths } from './get-project-paths';
|
|
||||||
export { DetectorFilesystem } from './detectors/filesystem';
|
|
||||||
export { readConfigFile } from './fs/read-config-file';
|
export { readConfigFile } from './fs/read-config-file';
|
||||||
export { normalizePath } from './fs/normalize-path';
|
export { normalizePath } from './fs/normalize-path';
|
||||||
|
|
||||||
@@ -97,35 +87,3 @@ export * from './should-serve';
|
|||||||
export * from './schemas';
|
export * from './schemas';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './errors';
|
export * from './errors';
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to support both `@vercel` and legacy `@now` official Runtimes.
|
|
||||||
*/
|
|
||||||
export const isOfficialRuntime = (desired: string, name?: string): boolean => {
|
|
||||||
if (typeof name !== 'string') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
name === `@vercel/${desired}` ||
|
|
||||||
name === `@now/${desired}` ||
|
|
||||||
name.startsWith(`@vercel/${desired}@`) ||
|
|
||||||
name.startsWith(`@now/${desired}@`)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isStaticRuntime = (name?: string): boolean => {
|
|
||||||
return isOfficialRuntime('static', name);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { workspaceManagers } from './workspaces/workspace-managers';
|
|
||||||
export {
|
|
||||||
getWorkspaces,
|
|
||||||
GetWorkspaceOptions,
|
|
||||||
Workspace,
|
|
||||||
WorkspaceType,
|
|
||||||
} from './workspaces/get-workspaces';
|
|
||||||
export {
|
|
||||||
getWorkspacePackagePaths,
|
|
||||||
GetWorkspacePackagePathsOptions,
|
|
||||||
} from './workspaces/get-workspace-package-paths';
|
|
||||||
export { monorepoManagers } from './monorepos/monorepo-managers';
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# users.rb
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"functions": {
|
|
||||||
"api/users.rb": {
|
|
||||||
"memory": 3008
|
|
||||||
},
|
|
||||||
"api/doesnt-exist.rb": {
|
|
||||||
"memory": 768
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# [id].py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# index
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# project/[aid]/[bid]/index.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# get
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# post
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
This file should also be included
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# date
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# math
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"functions": {
|
|
||||||
"api/users/post.py": {
|
|
||||||
"memory": 3008
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
148
packages/build-utils/test/integration.test.ts
vendored
148
packages/build-utils/test/integration.test.ts
vendored
@@ -5,7 +5,6 @@ import {
|
|||||||
testDeployment,
|
testDeployment,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from '../../../test/lib/deployment/test-deployment';
|
} from '../../../test/lib/deployment/test-deployment';
|
||||||
import { glob, detectBuilders } from '../src';
|
|
||||||
|
|
||||||
jest.setTimeout(4 * 60 * 1000);
|
jest.setTimeout(4 * 60 * 1000);
|
||||||
|
|
||||||
@@ -32,11 +31,6 @@ const skipFixtures: string[] = [
|
|||||||
'08-zero-config-middleman',
|
'08-zero-config-middleman',
|
||||||
'21-npm-workspaces',
|
'21-npm-workspaces',
|
||||||
'23-pnpm-workspaces',
|
'23-pnpm-workspaces',
|
||||||
'27-yarn-workspaces',
|
|
||||||
'28-turborepo-with-yarn-workspaces',
|
|
||||||
'29-nested-workspaces',
|
|
||||||
'30-double-nested-workspaces',
|
|
||||||
'31-turborepo-in-package-json',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
@@ -83,145 +77,3 @@ for (const builder of buildersToTestWith) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
it('Test `detectBuilders` and `detectRoutes`', async () => {
|
|
||||||
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
|
|
||||||
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
|
|
||||||
const fileList = await glob('**', fixture);
|
|
||||||
const files = Object.keys(fileList);
|
|
||||||
|
|
||||||
const probes = [
|
|
||||||
{
|
|
||||||
path: '/api/my-endpoint',
|
|
||||||
mustContain: 'my-endpoint',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/other-endpoint',
|
|
||||||
mustContain: 'other-endpoint',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/team/zeit',
|
|
||||||
mustContain: 'team/zeit',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/user/myself',
|
|
||||||
mustContain: 'user/myself',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/not-okay/',
|
|
||||||
status: 404,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api',
|
|
||||||
status: 404,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/',
|
|
||||||
status: 404,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
mustContain: 'hello from index.txt',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { builders, defaultRoutes } = await detectBuilders(files, pkg);
|
|
||||||
|
|
||||||
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
|
|
||||||
|
|
||||||
await fs.writeFile(
|
|
||||||
path.join(fixture, 'now.json'),
|
|
||||||
JSON.stringify(nowConfig, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
const deployment = await testDeployment(
|
|
||||||
{ builderUrl, buildUtilsUrl },
|
|
||||||
fixture
|
|
||||||
);
|
|
||||||
expect(deployment).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Test `detectBuilders` with `index` files', async () => {
|
|
||||||
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
|
|
||||||
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
|
|
||||||
const fileList = await glob('**', fixture);
|
|
||||||
const files = Object.keys(fileList);
|
|
||||||
|
|
||||||
const probes = [
|
|
||||||
{
|
|
||||||
path: '/api/not-okay',
|
|
||||||
status: 404,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api',
|
|
||||||
mustContain: 'hello from api/index.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/',
|
|
||||||
mustContain: 'hello from api/index.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/index',
|
|
||||||
mustContain: 'hello from api/index.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/index.js',
|
|
||||||
mustContain: 'hello from api/index.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/date.js',
|
|
||||||
mustContain: 'hello from api/date.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Someone might expect this to be `date.js`,
|
|
||||||
// but I doubt that there is any case were both
|
|
||||||
// `date/index.js` and `date.js` exists,
|
|
||||||
// so it is not special cased
|
|
||||||
path: '/api/date',
|
|
||||||
mustContain: 'hello from api/date/index.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/date/',
|
|
||||||
mustContain: 'hello from api/date/index.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/date/index',
|
|
||||||
mustContain: 'hello from api/date/index.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/api/date/index.js',
|
|
||||||
mustContain: 'hello from api/date/index.js',
|
|
||||||
status: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
mustContain: 'hello from index.txt',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const { builders, defaultRoutes } = await detectBuilders(files, pkg);
|
|
||||||
|
|
||||||
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
|
|
||||||
await fs.writeFile(
|
|
||||||
path.join(fixture, 'now.json'),
|
|
||||||
JSON.stringify(nowConfig, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
const deployment = await testDeployment(
|
|
||||||
{ builderUrl, buildUtilsUrl },
|
|
||||||
fixture
|
|
||||||
);
|
|
||||||
expect(deployment).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|||||||
29
packages/build-utils/test/unit.exec-async.test.ts
vendored
Normal file
29
packages/build-utils/test/unit.exec-async.test.ts
vendored
Normal 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'
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -38,6 +38,38 @@ describe('Test `getEnvForPackageManager()`', () => {
|
|||||||
PATH: `/node16/bin-npm7${delimiter}foo`,
|
PATH: `/node16/bin-npm7${delimiter}foo`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'should not set npm path if corepack enabled',
|
||||||
|
args: {
|
||||||
|
cliType: 'npm',
|
||||||
|
nodeVersion: { major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||||
|
lockfileVersion: 2,
|
||||||
|
env: {
|
||||||
|
FOO: 'bar',
|
||||||
|
ENABLE_EXPERIMENTAL_COREPACK: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: {
|
||||||
|
FOO: 'bar',
|
||||||
|
ENABLE_EXPERIMENTAL_COREPACK: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should not prepend npm path again if already detected',
|
||||||
|
args: {
|
||||||
|
cliType: 'npm',
|
||||||
|
nodeVersion: { major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||||
|
lockfileVersion: 2,
|
||||||
|
env: {
|
||||||
|
FOO: 'bar',
|
||||||
|
PATH: `/node16/bin-npm7${delimiter}foo`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: {
|
||||||
|
FOO: 'bar',
|
||||||
|
PATH: `/node16/bin-npm7${delimiter}foo`,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'should not set path if node is 16 and npm 7+ is detected',
|
name: 'should not set path if node is 16 and npm 7+ is detected',
|
||||||
args: {
|
args: {
|
||||||
@@ -101,6 +133,38 @@ describe('Test `getEnvForPackageManager()`', () => {
|
|||||||
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
|
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'should not set pnpm path if corepack is enabled',
|
||||||
|
args: {
|
||||||
|
cliType: 'pnpm',
|
||||||
|
nodeVersion: { major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
lockfileVersion: 5.4,
|
||||||
|
env: {
|
||||||
|
FOO: 'bar',
|
||||||
|
ENABLE_EXPERIMENTAL_COREPACK: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: {
|
||||||
|
FOO: 'bar',
|
||||||
|
ENABLE_EXPERIMENTAL_COREPACK: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should not prepend pnpm path again if already detected',
|
||||||
|
args: {
|
||||||
|
cliType: 'pnpm',
|
||||||
|
nodeVersion: { major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
lockfileVersion: 5.4,
|
||||||
|
env: {
|
||||||
|
FOO: 'bar',
|
||||||
|
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: {
|
||||||
|
FOO: 'bar',
|
||||||
|
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'should not set path if pnpm 6 is detected',
|
name: 'should not set path if pnpm 6 is detected',
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
111
packages/build-utils/test/unit.get-spawn-options.test.ts
vendored
Normal file
111
packages/build-utils/test/unit.get-spawn-options.test.ts
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { delimiter } from 'path';
|
||||||
|
import { getSpawnOptions } from '../src';
|
||||||
|
|
||||||
|
describe('Test `getSpawnOptions()`', () => {
|
||||||
|
const origProcessEnvPath = process.env.PATH;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.PATH = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.PATH = origProcessEnvPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cases: Array<{
|
||||||
|
name: string;
|
||||||
|
args: Parameters<typeof getSpawnOptions>;
|
||||||
|
envPath: string | undefined;
|
||||||
|
want: string | undefined;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
name: 'should do nothing when isDev and node14',
|
||||||
|
args: [
|
||||||
|
{ isDev: true },
|
||||||
|
{ major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||||
|
],
|
||||||
|
envPath: '/foo',
|
||||||
|
want: '/foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should do nothing when isDev and node16',
|
||||||
|
args: [
|
||||||
|
{ isDev: true },
|
||||||
|
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
],
|
||||||
|
envPath: '/foo',
|
||||||
|
want: '/foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should replace 14 with 16 when only path',
|
||||||
|
args: [
|
||||||
|
{ isDev: false },
|
||||||
|
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
],
|
||||||
|
envPath: '/node14/bin',
|
||||||
|
want: '/node16/bin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should replace 14 with 16 at beginning',
|
||||||
|
args: [
|
||||||
|
{ isDev: false },
|
||||||
|
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
],
|
||||||
|
envPath: `/node14/bin${delimiter}/foo`,
|
||||||
|
want: `/node16/bin${delimiter}/foo`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should replace 14 with 16 at end',
|
||||||
|
args: [
|
||||||
|
{ isDev: false },
|
||||||
|
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
],
|
||||||
|
envPath: `/foo${delimiter}/node14/bin`,
|
||||||
|
want: `/foo${delimiter}/node16/bin`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should replace 14 with 16 in middle',
|
||||||
|
args: [
|
||||||
|
{ isDev: false },
|
||||||
|
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
],
|
||||||
|
envPath: `/foo${delimiter}/node14/bin${delimiter}/bar`,
|
||||||
|
want: `/foo${delimiter}/node16/bin${delimiter}/bar`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should prepend 16 at beginning when nothing to replace',
|
||||||
|
args: [
|
||||||
|
{ isDev: false },
|
||||||
|
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
],
|
||||||
|
envPath: `/foo`,
|
||||||
|
want: `/node16/bin${delimiter}/foo`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should prepend 16 at beginning no path input',
|
||||||
|
args: [
|
||||||
|
{ isDev: false },
|
||||||
|
{ major: 16, range: '16.x', runtime: 'nodejs16.x' },
|
||||||
|
],
|
||||||
|
envPath: '',
|
||||||
|
want: `/node16/bin`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should replace 12 with 14 when only path',
|
||||||
|
args: [
|
||||||
|
{ isDev: false },
|
||||||
|
{ major: 14, range: '14.x', runtime: 'nodejs14.x' },
|
||||||
|
],
|
||||||
|
envPath: '/node12/bin',
|
||||||
|
want: '/node14/bin',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { name, args, envPath, want } of cases) {
|
||||||
|
it(name, () => {
|
||||||
|
process.env.PATH = envPath;
|
||||||
|
const opts = getSpawnOptions(...args);
|
||||||
|
expect(opts.env?.PATH).toBe(want);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
21
packages/build-utils/test/unit.spawn-async.test.ts
vendored
Normal file
21
packages/build-utils/test/unit.spawn-async.test.ts
vendored
Normal 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();
|
||||||
|
});
|
||||||
2
packages/build-utils/test/unit.test.ts
vendored
2
packages/build-utils/test/unit.test.ts
vendored
@@ -18,6 +18,8 @@ import {
|
|||||||
Meta,
|
Meta,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
|
jest.setTimeout(7 * 1000);
|
||||||
|
|
||||||
async function expectBuilderError(promise: Promise<any>, pattern: string) {
|
async function expectBuilderError(promise: Promise<any>, pattern: string) {
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vercel",
|
"name": "vercel",
|
||||||
"version": "25.2.0",
|
"version": "26.0.0",
|
||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "The command-line interface for Vercel",
|
"description": "The command-line interface for Vercel",
|
||||||
@@ -42,15 +42,15 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/build-utils": "4.2.0",
|
"@vercel/build-utils": "5.0.0",
|
||||||
"@vercel/go": "2.0.2",
|
"@vercel/go": "2.0.4",
|
||||||
"@vercel/next": "3.1.0",
|
"@vercel/next": "3.1.3",
|
||||||
"@vercel/node": "2.3.0",
|
"@vercel/node": "2.4.0",
|
||||||
"@vercel/python": "3.0.2",
|
"@vercel/python": "3.0.4",
|
||||||
"@vercel/redwood": "1.0.2",
|
"@vercel/redwood": "1.0.5",
|
||||||
"@vercel/remix": "1.0.2",
|
"@vercel/remix": "1.0.5",
|
||||||
"@vercel/ruby": "1.3.10",
|
"@vercel/ruby": "1.3.12",
|
||||||
"@vercel/static-build": "1.0.2",
|
"@vercel/static-build": "1.0.4",
|
||||||
"update-notifier": "5.1.0"
|
"update-notifier": "5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -95,8 +95,9 @@
|
|||||||
"@types/which": "1.3.2",
|
"@types/which": "1.3.2",
|
||||||
"@types/write-json-file": "2.2.1",
|
"@types/write-json-file": "2.2.1",
|
||||||
"@types/yauzl-promise": "2.1.0",
|
"@types/yauzl-promise": "2.1.0",
|
||||||
"@vercel/client": "12.0.2",
|
"@vercel/client": "12.0.4",
|
||||||
"@vercel/frameworks": "1.0.2",
|
"@vercel/frameworks": "1.0.2",
|
||||||
|
"@vercel/fs-detectors": "1.0.0",
|
||||||
"@vercel/ncc": "0.24.0",
|
"@vercel/ncc": "0.24.0",
|
||||||
"@zeit/fun": "0.11.2",
|
"@zeit/fun": "0.11.2",
|
||||||
"@zeit/source-map-support": "0.6.2",
|
"@zeit/source-map-support": "0.6.2",
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import confirm from '../../util/input/confirm';
|
|||||||
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
|
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
|
||||||
|
|
||||||
import { Alias } from '../../types';
|
import { Alias } from '../../types';
|
||||||
import { Output } from '../../util/output';
|
|
||||||
import { isValidName } from '../../util/is-valid-name';
|
import { isValidName } from '../../util/is-valid-name';
|
||||||
import { getCommandName } from '../../util/pkg-name';
|
import { getCommandName } from '../../util/pkg-name';
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ export default async function rm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeStamp = stamp();
|
const removeStamp = stamp();
|
||||||
if (!opts['--yes'] && !(await confirmAliasRemove(output, alias))) {
|
if (!opts['--yes'] && !(await confirmAliasRemove(client, alias))) {
|
||||||
output.log('Aborted');
|
output.log('Aborted');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -85,7 +84,7 @@ export default async function rm(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmAliasRemove(output: Output, alias: Alias) {
|
async function confirmAliasRemove(client: Client, alias: Alias) {
|
||||||
const srcUrl = alias.deployment
|
const srcUrl = alias.deployment
|
||||||
? chalk.underline(alias.deployment.url)
|
? chalk.underline(alias.deployment.url)
|
||||||
: null;
|
: null;
|
||||||
@@ -104,7 +103,7 @@ async function confirmAliasRemove(output: Output, alias: Alias) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
output.log(`The following alias will be removed permanently`);
|
client.output.log(`The following alias will be removed permanently`);
|
||||||
output.print(` ${tbl}\n`);
|
client.output.print(` ${tbl}\n`);
|
||||||
return confirm(chalk.red('Are you sure?'), false);
|
return confirm(client, chalk.red('Are you sure?'), false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export default async function ({ creditCards, clear = false, contextName }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(''); // New line
|
console.log(''); // New line
|
||||||
const stopSpinner = wait('Saving card');
|
const stopSpinner = wait(process.stderr, 'Saving card');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await creditCards.add({
|
const res = await creditCards.add({
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export default async client => {
|
|||||||
)} ${chalk.gray(`[${elapsed}]`)}`;
|
)} ${chalk.gray(`[${elapsed}]`)}`;
|
||||||
const choices = buildInquirerChoices(cards);
|
const choices = buildInquirerChoices(cards);
|
||||||
|
|
||||||
cardId = await listInput({
|
cardId = await listInput(client, {
|
||||||
message,
|
message,
|
||||||
choices,
|
choices,
|
||||||
separator: true,
|
separator: true,
|
||||||
@@ -187,6 +187,7 @@ export default async client => {
|
|||||||
if (cardId) {
|
if (cardId) {
|
||||||
const label = `Are you sure that you to set this card as the default?`;
|
const label = `Are you sure that you to set this card as the default?`;
|
||||||
const confirmation = await promptBool(label, {
|
const confirmation = await promptBool(label, {
|
||||||
|
...client,
|
||||||
trailing: '\n',
|
trailing: '\n',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,7 +251,7 @@ export default async client => {
|
|||||||
)} under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`;
|
)} under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`;
|
||||||
const choices = buildInquirerChoices(cards);
|
const choices = buildInquirerChoices(cards);
|
||||||
|
|
||||||
cardId = await listInput({
|
cardId = await listInput(client, {
|
||||||
message,
|
message,
|
||||||
choices,
|
choices,
|
||||||
separator: true,
|
separator: true,
|
||||||
@@ -262,7 +263,7 @@ export default async client => {
|
|||||||
// typed `vercel billing rm <some-id>`) is valid
|
// typed `vercel billing rm <some-id>`) is valid
|
||||||
if (cardId) {
|
if (cardId) {
|
||||||
const label = `Are you sure that you want to remove this card?`;
|
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) {
|
if (!confirmation) {
|
||||||
console.log('Aborted');
|
console.log('Aborted');
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import logo from '../../util/output/logo';
|
|||||||
import getArgs from '../../util/get-args';
|
import getArgs from '../../util/get-args';
|
||||||
import Client from '../../util/client';
|
import Client from '../../util/client';
|
||||||
import { getPkgName } from '../../util/pkg-name';
|
import { getPkgName } from '../../util/pkg-name';
|
||||||
import { Output } from '../../util/output';
|
|
||||||
import { Deployment, PaginationOptions } from '../../types';
|
import { Deployment, PaginationOptions } from '../../types';
|
||||||
import { normalizeURL } from '../../util/bisect/normalize-url';
|
import { normalizeURL } from '../../util/bisect/normalize-url';
|
||||||
|
|
||||||
@@ -86,10 +85,10 @@ export default async function main(client: Client): Promise<number> {
|
|||||||
|
|
||||||
let bad =
|
let bad =
|
||||||
argv['--bad'] ||
|
argv['--bad'] ||
|
||||||
(await prompt(output, `Specify a URL where the bug occurs:`));
|
(await prompt(client, `Specify a URL where the bug occurs:`));
|
||||||
let good =
|
let good =
|
||||||
argv['--good'] ||
|
argv['--good'] ||
|
||||||
(await prompt(output, `Specify a URL where the bug does not occur:`));
|
(await prompt(client, `Specify a URL where the bug does not occur:`));
|
||||||
let subpath = argv['--path'] || '';
|
let subpath = argv['--path'] || '';
|
||||||
let run = argv['--run'] || '';
|
let run = argv['--run'] || '';
|
||||||
const openEnabled = argv['--open'] || false;
|
const openEnabled = argv['--open'] || false;
|
||||||
@@ -143,7 +142,7 @@ export default async function main(client: Client): Promise<number> {
|
|||||||
|
|
||||||
if (!subpath) {
|
if (!subpath) {
|
||||||
subpath = await prompt(
|
subpath = await prompt(
|
||||||
output,
|
client,
|
||||||
`Specify the URL subpath where the bug occurs:`
|
`Specify the URL subpath where the bug occurs:`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -391,10 +390,10 @@ function getCommit(deployment: DeploymentV6) {
|
|||||||
return { sha, message };
|
return { sha, message };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prompt(output: Output, message: string): Promise<string> {
|
async function prompt(client: Client, message: string): Promise<string> {
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
const { val } = await inquirer.prompt({
|
const { val } = await client.prompt({
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'val',
|
name: 'val',
|
||||||
message,
|
message,
|
||||||
@@ -402,7 +401,7 @@ async function prompt(output: Output, message: string): Promise<string> {
|
|||||||
if (val) {
|
if (val) {
|
||||||
return val;
|
return val;
|
||||||
} else {
|
} else {
|
||||||
output.error('A value must be specified');
|
client.output.error('A value must be specified');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import chalk from 'chalk';
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { join, normalize, relative, resolve } from 'path';
|
import { join, normalize, relative, resolve } from 'path';
|
||||||
import {
|
import {
|
||||||
detectBuilders,
|
|
||||||
normalizePath,
|
normalizePath,
|
||||||
Files,
|
Files,
|
||||||
FileFsRef,
|
FileFsRef,
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
BuildResultV3,
|
BuildResultV3,
|
||||||
NowBuildError,
|
NowBuildError,
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
|
import { detectBuilders } from '@vercel/fs-detectors';
|
||||||
import minimatch from 'minimatch';
|
import minimatch from 'minimatch';
|
||||||
import {
|
import {
|
||||||
appendRoutesToPhase,
|
appendRoutesToPhase,
|
||||||
@@ -140,6 +140,7 @@ export default async function main(client: Client): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirmed = await confirm(
|
confirmed = await confirm(
|
||||||
|
client,
|
||||||
`No Project Settings found locally. Run ${cli.getCommandName(
|
`No Project Settings found locally. Run ${cli.getCommandName(
|
||||||
'pull'
|
'pull'
|
||||||
)} for retrieving them?`,
|
)} for retrieving them?`,
|
||||||
|
|||||||
@@ -160,12 +160,12 @@ export default async (client: Client) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { log, debug, error, prettyError, isTTY } = output;
|
const { log, debug, error, prettyError } = output;
|
||||||
|
|
||||||
const quiet = !isTTY;
|
const quiet = !client.stdout.isTTY;
|
||||||
|
|
||||||
// check paths
|
// check paths
|
||||||
const pathValidation = await validatePaths(output, paths);
|
const pathValidation = await validatePaths(client, paths);
|
||||||
|
|
||||||
if (!pathValidation.valid) {
|
if (!pathValidation.valid) {
|
||||||
return pathValidation.exitCode;
|
return pathValidation.exitCode;
|
||||||
@@ -243,6 +243,7 @@ export default async (client: Client) => {
|
|||||||
const shouldStartSetup =
|
const shouldStartSetup =
|
||||||
autoConfirm ||
|
autoConfirm ||
|
||||||
(await confirm(
|
(await confirm(
|
||||||
|
client,
|
||||||
`Set up and deploy ${chalk.cyan(`“${toHumanPath(path)}”`)}?`,
|
`Set up and deploy ${chalk.cyan(`“${toHumanPath(path)}”`)}?`,
|
||||||
true
|
true
|
||||||
));
|
));
|
||||||
@@ -287,7 +288,7 @@ export default async (client: Client) => {
|
|||||||
|
|
||||||
if (typeof projectOrNewProjectName === 'string') {
|
if (typeof projectOrNewProjectName === 'string') {
|
||||||
newProjectName = projectOrNewProjectName;
|
newProjectName = projectOrNewProjectName;
|
||||||
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
|
rootDirectory = await inputRootDirectory(client, path, autoConfirm);
|
||||||
} else {
|
} else {
|
||||||
project = projectOrNewProjectName;
|
project = projectOrNewProjectName;
|
||||||
rootDirectory = project.rootDirectory;
|
rootDirectory = project.rootDirectory;
|
||||||
@@ -521,7 +522,7 @@ export default async (client: Client) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const settings = await editProjectSettings(
|
const settings = await editProjectSettings(
|
||||||
output,
|
client,
|
||||||
projectSettings,
|
projectSettings,
|
||||||
framework,
|
framework,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default async function add(
|
|||||||
|
|
||||||
const addStamp = stamp();
|
const addStamp = stamp();
|
||||||
const { domain, data: argData } = parsedParams;
|
const { domain, data: argData } = parsedParams;
|
||||||
const data = await getDNSData(output, argData);
|
const data = await getDNSData(client, argData);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
output.log(`Aborted`);
|
output.log(`Aborted`);
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ export default async function buy(
|
|||||||
!(await promptBool(
|
!(await promptBool(
|
||||||
`Buy now for ${chalk.bold(`$${price}`)} (${`${period}yr${
|
`Buy now for ${chalk.bold(`$${price}`)} (${`${period}yr${
|
||||||
period > 1 ? 's' : ''
|
period > 1 ? 's' : ''
|
||||||
}`})?`
|
}`})?`,
|
||||||
|
client
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -99,7 +100,7 @@ export default async function buy(
|
|||||||
: `Auto renew every ${renewalPrice.period} years for ${chalk.bold(
|
: `Auto renew every ${renewalPrice.period} years for ${chalk.bold(
|
||||||
`$${price}`
|
`$${price}`
|
||||||
)}?`,
|
)}?`,
|
||||||
{ defaultValue: true }
|
{ ...client, defaultValue: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
let buyResult;
|
let buyResult;
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ export default async function move(
|
|||||||
!(await promptBool(
|
!(await promptBool(
|
||||||
`Are you sure you want to move ${param(domainName)} to ${param(
|
`Are you sure you want to move ${param(domainName)} to ${param(
|
||||||
destination
|
destination
|
||||||
)}?`
|
)}?`,
|
||||||
|
client
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
output.log('Aborted');
|
output.log('Aborted');
|
||||||
@@ -95,7 +96,8 @@ export default async function move(
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!(await promptBool(
|
!(await promptBool(
|
||||||
`Are you sure you want to move ${param(domainName)}?`
|
`Are you sure you want to move ${param(domainName)}?`,
|
||||||
|
client
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
output.log('Aborted');
|
output.log('Aborted');
|
||||||
|
|||||||
@@ -92,7 +92,10 @@ export default async function rm(
|
|||||||
const skipConfirmation = opts['--yes'] || false;
|
const skipConfirmation = opts['--yes'] || false;
|
||||||
if (
|
if (
|
||||||
!skipConfirmation &&
|
!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');
|
output.log('Aborted');
|
||||||
return 0;
|
return 0;
|
||||||
@@ -230,7 +233,7 @@ async function removeDomain(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!skipConfirmation &&
|
!skipConfirmation &&
|
||||||
!(await promptBool(`Remove conflicts associated with domain?`))
|
!(await promptBool(`Remove conflicts associated with domain?`, client))
|
||||||
) {
|
) {
|
||||||
output.log('Aborted');
|
output.log('Aborted');
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ export default async function transferIn(
|
|||||||
const shouldTransfer = await promptBool(
|
const shouldTransfer = await promptBool(
|
||||||
transferPolicy === 'no-change'
|
transferPolicy === 'no-change'
|
||||||
? `Transfer now for ${chalk.bold(`$${price}`)}?`
|
? `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) {
|
if (!shouldTransfer) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
2
packages/cli/src/commands/env/add.ts
vendored
2
packages/cli/src/commands/env/add.ts
vendored
@@ -31,7 +31,7 @@ export default async function add(
|
|||||||
// improve the way we show inquirer prompts
|
// improve the way we show inquirer prompts
|
||||||
require('../../util/input/patch-inquirer');
|
require('../../util/input/patch-inquirer');
|
||||||
|
|
||||||
const stdInput = await readStandardInput();
|
const stdInput = await readStandardInput(client.stdin);
|
||||||
let [envName, envTargetArg, envGitBranch] = args;
|
let [envName, envTargetArg, envGitBranch] = args;
|
||||||
|
|
||||||
if (args.length > 3) {
|
if (args.length > 3) {
|
||||||
|
|||||||
1
packages/cli/src/commands/env/pull.ts
vendored
1
packages/cli/src/commands/env/pull.ts
vendored
@@ -74,6 +74,7 @@ export default async function pull(
|
|||||||
exists &&
|
exists &&
|
||||||
!skipConfirmation &&
|
!skipConfirmation &&
|
||||||
!(await confirm(
|
!(await confirm(
|
||||||
|
client,
|
||||||
`Found existing file ${param(filename)}. Do you want to overwrite?`,
|
`Found existing file ${param(filename)}. Do you want to overwrite?`,
|
||||||
false
|
false
|
||||||
))
|
))
|
||||||
|
|||||||
1
packages/cli/src/commands/env/rm.ts
vendored
1
packages/cli/src/commands/env/rm.ts
vendored
@@ -104,6 +104,7 @@ export default async function rm(
|
|||||||
if (
|
if (
|
||||||
!skipConfirmation &&
|
!skipConfirmation &&
|
||||||
!(await confirm(
|
!(await confirm(
|
||||||
|
client,
|
||||||
`Removing Environment Variable ${param(env.key)} from ${formatEnvTarget(
|
`Removing Environment Variable ${param(env.key)} from ${formatEnvTarget(
|
||||||
env
|
env
|
||||||
)} in Project ${chalk.bold(project.name)}. Are you sure?`,
|
)} in Project ${chalk.bold(project.name)}. Are you sure?`,
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ export default async function init(
|
|||||||
const exampleList = examples.filter(x => x.visible).map(x => x.name);
|
const exampleList = examples.filter(x => x.visible).map(x => x.name);
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
const chosen = await chooseFromDropdown('Select example:', exampleList);
|
const chosen = await chooseFromDropdown(
|
||||||
|
client,
|
||||||
|
'Select example:',
|
||||||
|
exampleList
|
||||||
|
);
|
||||||
|
|
||||||
if (!chosen) {
|
if (!chosen) {
|
||||||
output.log('Aborted');
|
output.log('Aborted');
|
||||||
@@ -65,7 +69,7 @@ export default async function init(
|
|||||||
return extractExample(client, name, dir, force, 'v1');
|
return extractExample(client, name, dir, force, 'v1');
|
||||||
}
|
}
|
||||||
|
|
||||||
const found = await guess(exampleList, name);
|
const found = await guess(client, exampleList, name);
|
||||||
|
|
||||||
if (typeof found === 'string') {
|
if (typeof found === 'string') {
|
||||||
return extractExample(client, found, dir, force);
|
return extractExample(client, found, dir, force);
|
||||||
@@ -90,14 +94,18 @@ async function fetchExampleList(client: Client) {
|
|||||||
/**
|
/**
|
||||||
* Prompt user for choosing which example to init
|
* Prompt user for choosing which example to init
|
||||||
*/
|
*/
|
||||||
async function chooseFromDropdown(message: string, exampleList: string[]) {
|
async function chooseFromDropdown(
|
||||||
|
client: Client,
|
||||||
|
message: string,
|
||||||
|
exampleList: string[]
|
||||||
|
) {
|
||||||
const choices = exampleList.map(name => ({
|
const choices = exampleList.map(name => ({
|
||||||
name,
|
name,
|
||||||
value: name,
|
value: name,
|
||||||
short: name,
|
short: name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return listInput({
|
return listInput(client, {
|
||||||
message,
|
message,
|
||||||
choices,
|
choices,
|
||||||
});
|
});
|
||||||
@@ -194,7 +202,7 @@ function prepareFolder(cwd: string, folder: string, force?: boolean) {
|
|||||||
/**
|
/**
|
||||||
* Guess which example user try to init
|
* 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(
|
const GuessError = new Error(
|
||||||
`No example found for ${chalk.bold(name)}, run ${getCommandName(
|
`No example found for ${chalk.bold(name)}, run ${getCommandName(
|
||||||
`init`
|
`init`
|
||||||
@@ -208,7 +216,7 @@ async function guess(exampleList: string[], name: string) {
|
|||||||
const found = didYouMean(name, exampleList, 0.7);
|
const found = didYouMean(name, exampleList, 0.7);
|
||||||
|
|
||||||
if (typeof found === 'string') {
|
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;
|
return found;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default async function main(client: Client, desiredSlug?: string) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
output.stopSpinner();
|
output.stopSpinner();
|
||||||
desiredSlug = await listInput({
|
desiredSlug = await listInput(client, {
|
||||||
message: 'Switch to:',
|
message: 'Switch to:',
|
||||||
choices,
|
choices,
|
||||||
eraseFinalAnswer: true,
|
eraseFinalAnswer: true,
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ export default async (client: Client): Promise<number> => {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.isTTY) {
|
if (client.stdout.isTTY) {
|
||||||
output.log(contextName);
|
output.log(contextName);
|
||||||
} else {
|
} else {
|
||||||
// If stdout is not a TTY, then only print the username
|
// If stdout is not a TTY, then only print the username
|
||||||
// to support piping the output to another file / exe
|
// to support piping the output to another file / exe
|
||||||
output.print(`${contextName}\n`, { w: process.stdout });
|
client.stdout.write(`${contextName}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import * as Sentry from '@sentry/node';
|
|||||||
import hp from './util/humanize-path';
|
import hp from './util/humanize-path';
|
||||||
import commands from './commands';
|
import commands from './commands';
|
||||||
import pkg from './util/pkg';
|
import pkg from './util/pkg';
|
||||||
import createOutput from './util/output';
|
import { Output } from './util/output';
|
||||||
import cmd from './util/output/cmd';
|
import cmd from './util/output/cmd';
|
||||||
import info from './util/output/info';
|
import info from './util/output/info';
|
||||||
import error from './util/output/error';
|
import error from './util/output/error';
|
||||||
@@ -109,7 +109,7 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDebugging = argv['--debug'];
|
const isDebugging = argv['--debug'];
|
||||||
const output = createOutput({ debug: isDebugging });
|
const output = new Output(process.stderr, { debug: isDebugging });
|
||||||
|
|
||||||
debug = output.debug;
|
debug = output.debug;
|
||||||
|
|
||||||
@@ -387,6 +387,9 @@ const main = async () => {
|
|||||||
// Shared API `Client` instance for all sub-commands to utilize
|
// Shared API `Client` instance for all sub-commands to utilize
|
||||||
client = new Client({
|
client = new Client({
|
||||||
apiUrl,
|
apiUrl,
|
||||||
|
stdin: process.stdin,
|
||||||
|
stdout: process.stdout,
|
||||||
|
stderr: output.stream,
|
||||||
output,
|
output,
|
||||||
config,
|
config,
|
||||||
authConfig,
|
authConfig,
|
||||||
@@ -796,7 +799,5 @@ process.on('uncaughtException', handleUnexpected);
|
|||||||
main()
|
main()
|
||||||
.then(exitCode => {
|
.then(exitCode => {
|
||||||
process.exitCode = exitCode;
|
process.exitCode = exitCode;
|
||||||
// @ts-ignore - "nowExit" is a non-standard event name
|
|
||||||
process.emit('nowExit');
|
|
||||||
})
|
})
|
||||||
.catch(handleUnexpected);
|
.catch(handleUnexpected);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Readable, Writable } from 'stream';
|
||||||
|
|
||||||
export type ProjectSettings = import('@vercel/build-utils').ProjectSettings;
|
export type ProjectSettings = import('@vercel/build-utils').ProjectSettings;
|
||||||
|
|
||||||
export type Primitive =
|
export type Primitive =
|
||||||
@@ -442,3 +444,19 @@ export interface BuildOutput {
|
|||||||
layers?: string[];
|
layers?: string[];
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReadableTTY extends Readable {
|
||||||
|
isTTY?: boolean;
|
||||||
|
isRaw?: boolean;
|
||||||
|
setRawMode?: (mode: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WritableTTY extends Writable {
|
||||||
|
isTTY?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stdio {
|
||||||
|
stdin: ReadableTTY;
|
||||||
|
stdout: WritableTTY;
|
||||||
|
stderr: WritableTTY;
|
||||||
|
}
|
||||||
|
|||||||
@@ -385,8 +385,14 @@ export async function* findDirs(
|
|||||||
}
|
}
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
const abs = join(dir, path);
|
const abs = join(dir, path);
|
||||||
const s = await fs.stat(abs);
|
let stat: fs.Stats;
|
||||||
if (s.isDirectory()) {
|
try {
|
||||||
|
stat = await fs.lstat(abs);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.code === 'ENOENT') continue;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (stat.isDirectory()) {
|
||||||
if (path === name) {
|
if (path === name) {
|
||||||
yield relative(root, abs);
|
yield relative(root, abs);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { bold } from 'chalk';
|
||||||
|
import inquirer from 'inquirer';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { URLSearchParams } from 'url';
|
import { URLSearchParams } from 'url';
|
||||||
import { parse as parseUrl } from 'url';
|
import { parse as parseUrl } from 'url';
|
||||||
@@ -11,10 +13,16 @@ import printIndications from './print-indications';
|
|||||||
import reauthenticate from './login/reauthenticate';
|
import reauthenticate from './login/reauthenticate';
|
||||||
import { SAMLError } from './login/types';
|
import { SAMLError } from './login/types';
|
||||||
import { writeToAuthConfigFile } from './config/files';
|
import { writeToAuthConfigFile } from './config/files';
|
||||||
import { AuthConfig, GlobalConfig, JSONObject } from '../types';
|
import type {
|
||||||
|
AuthConfig,
|
||||||
|
GlobalConfig,
|
||||||
|
JSONObject,
|
||||||
|
Stdio,
|
||||||
|
ReadableTTY,
|
||||||
|
WritableTTY,
|
||||||
|
} from '../types';
|
||||||
import { sharedPromise } from './promise';
|
import { sharedPromise } from './promise';
|
||||||
import { APIError } from './errors-ts';
|
import { APIError } from './errors-ts';
|
||||||
import { bold } from 'chalk';
|
|
||||||
|
|
||||||
const isSAMLError = (v: any): v is SAMLError => {
|
const isSAMLError = (v: any): v is SAMLError => {
|
||||||
return v && v.saml;
|
return v && v.saml;
|
||||||
@@ -28,7 +36,7 @@ export interface FetchOptions extends Omit<RequestInit, 'body'> {
|
|||||||
accountId?: string;
|
accountId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientOptions {
|
export interface ClientOptions extends Stdio {
|
||||||
argv: string[];
|
argv: string[];
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
authConfig: AuthConfig;
|
authConfig: AuthConfig;
|
||||||
@@ -41,13 +49,17 @@ export const isJSONObject = (v: any): v is JSONObject => {
|
|||||||
return v && typeof v == 'object' && v.constructor === Object;
|
return v && typeof v == 'object' && v.constructor === Object;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Client extends EventEmitter {
|
export default class Client extends EventEmitter implements Stdio {
|
||||||
argv: string[];
|
argv: string[];
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
authConfig: AuthConfig;
|
authConfig: AuthConfig;
|
||||||
|
stdin: ReadableTTY;
|
||||||
|
stdout: WritableTTY;
|
||||||
|
stderr: WritableTTY;
|
||||||
output: Output;
|
output: Output;
|
||||||
config: GlobalConfig;
|
config: GlobalConfig;
|
||||||
localConfig?: VercelConfig;
|
localConfig?: VercelConfig;
|
||||||
|
prompt!: inquirer.PromptModule;
|
||||||
private requestIdCounter: number;
|
private requestIdCounter: number;
|
||||||
|
|
||||||
constructor(opts: ClientOptions) {
|
constructor(opts: ClientOptions) {
|
||||||
@@ -55,10 +67,14 @@ export default class Client extends EventEmitter {
|
|||||||
this.argv = opts.argv;
|
this.argv = opts.argv;
|
||||||
this.apiUrl = opts.apiUrl;
|
this.apiUrl = opts.apiUrl;
|
||||||
this.authConfig = opts.authConfig;
|
this.authConfig = opts.authConfig;
|
||||||
|
this.stdin = opts.stdin;
|
||||||
|
this.stdout = opts.stdout;
|
||||||
|
this.stderr = opts.stderr;
|
||||||
this.output = opts.output;
|
this.output = opts.output;
|
||||||
this.config = opts.config;
|
this.config = opts.config;
|
||||||
this.localConfig = opts.localConfig;
|
this.localConfig = opts.localConfig;
|
||||||
this.requestIdCounter = 1;
|
this.requestIdCounter = 1;
|
||||||
|
this._createPromptModule();
|
||||||
}
|
}
|
||||||
|
|
||||||
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
|
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
|
||||||
@@ -124,7 +140,7 @@ export default class Client extends EventEmitter {
|
|||||||
return this.retry(async bail => {
|
return this.retry(async bail => {
|
||||||
const res = await this._fetch(url, opts);
|
const res = await this._fetch(url, opts);
|
||||||
|
|
||||||
printIndications(res);
|
printIndications(this, res);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await responseError(res);
|
const error = await responseError(res);
|
||||||
@@ -180,4 +196,11 @@ export default class Client extends EventEmitter {
|
|||||||
_onRetry = (error: Error) => {
|
_onRetry = (error: Error) => {
|
||||||
this.output.debug(`Retrying: ${error}\n${error.stack}`);
|
this.output.debug(`Retrying: ${error}\n${error.stack}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_createPromptModule() {
|
||||||
|
this.prompt = inquirer.createPromptModule({
|
||||||
|
input: this.stdin as NodeJS.ReadStream,
|
||||||
|
output: this.stderr as NodeJS.WriteStream,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
Lambda,
|
Lambda,
|
||||||
FileBlob,
|
FileBlob,
|
||||||
FileFsRef,
|
FileFsRef,
|
||||||
isOfficialRuntime,
|
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
|
import { isOfficialRuntime } from '@vercel/fs-detectors';
|
||||||
import plural from 'pluralize';
|
import plural from 'pluralize';
|
||||||
import minimatch from 'minimatch';
|
import minimatch from 'minimatch';
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ import {
|
|||||||
StartDevServerResult,
|
StartDevServerResult,
|
||||||
FileFsRef,
|
FileFsRef,
|
||||||
PackageJson,
|
PackageJson,
|
||||||
|
spawnCommand,
|
||||||
|
} from '@vercel/build-utils';
|
||||||
|
import {
|
||||||
detectBuilders,
|
detectBuilders,
|
||||||
detectApiDirectory,
|
detectApiDirectory,
|
||||||
detectApiExtensions,
|
detectApiExtensions,
|
||||||
spawnCommand,
|
|
||||||
isOfficialRuntime,
|
isOfficialRuntime,
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/fs-detectors';
|
||||||
import frameworkList from '@vercel/frameworks';
|
import frameworkList from '@vercel/frameworks';
|
||||||
|
|
||||||
import cmd from '../output/cmd';
|
import cmd from '../output/cmd';
|
||||||
@@ -329,6 +331,8 @@ export default class DevServer {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const name = relative(this.cwd, fsPath);
|
const name = relative(this.cwd, fsPath);
|
||||||
try {
|
try {
|
||||||
|
await this.getVercelConfig();
|
||||||
|
|
||||||
this.files[name] = await FileFsRef.fromFsPath({ fsPath });
|
this.files[name] = await FileFsRef.fromFsPath({ fsPath });
|
||||||
const extensionless = this.getExtensionlessFile(name);
|
const extensionless = this.getExtensionlessFile(name);
|
||||||
if (extensionless) {
|
if (extensionless) {
|
||||||
|
|||||||
@@ -2,26 +2,29 @@ import chalk from 'chalk';
|
|||||||
import { DNSRecordData } from '../../types';
|
import { DNSRecordData } from '../../types';
|
||||||
import textInput from '../input/text';
|
import textInput from '../input/text';
|
||||||
import promptBool from '../input/prompt-bool';
|
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'];
|
const RECORD_TYPES = ['A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'SRV', 'TXT'];
|
||||||
|
|
||||||
export default async function getDNSData(
|
export default async function getDNSData(
|
||||||
output: Output,
|
client: Client,
|
||||||
data: null | DNSRecordData
|
data: null | DNSRecordData
|
||||||
): Promise<DNSRecordData | null> {
|
): Promise<DNSRecordData | null> {
|
||||||
if (data) {
|
if (data) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
const { output } = client;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// first ask for type, branch from there
|
// first ask for type, branch from there
|
||||||
const possibleTypes = new Set(RECORD_TYPES);
|
const possibleTypes = new Set(RECORD_TYPES);
|
||||||
const type = (await textInput({
|
const type = (
|
||||||
label: `- Record type (${RECORD_TYPES.join(', ')}): `,
|
await textInput({
|
||||||
validateValue: (v: string) =>
|
label: `- Record type (${RECORD_TYPES.join(', ')}): `,
|
||||||
Boolean(v && possibleTypes.has(v.trim().toUpperCase()))
|
validateValue: (v: string) =>
|
||||||
}))
|
Boolean(v && possibleTypes.has(v.trim().toUpperCase())),
|
||||||
|
})
|
||||||
|
)
|
||||||
.trim()
|
.trim()
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
|
|
||||||
@@ -39,7 +42,7 @@ export default async function getDNSData(
|
|||||||
target
|
target
|
||||||
)}.`
|
)}.`
|
||||||
);
|
);
|
||||||
return (await verifyData())
|
return (await verifyData(client))
|
||||||
? {
|
? {
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
@@ -47,8 +50,8 @@ export default async function getDNSData(
|
|||||||
priority,
|
priority,
|
||||||
weight,
|
weight,
|
||||||
port,
|
port,
|
||||||
target
|
target,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -61,23 +64,23 @@ export default async function getDNSData(
|
|||||||
`${mxPriority}`
|
`${mxPriority}`
|
||||||
)} ${chalk.cyan(value)}`
|
)} ${chalk.cyan(value)}`
|
||||||
);
|
);
|
||||||
return (await verifyData())
|
return (await verifyData(client))
|
||||||
? {
|
? {
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
value,
|
value,
|
||||||
mxPriority
|
mxPriority,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = await getTrimmedString(`- ${type} value: `);
|
const value = await getTrimmedString(`- ${type} value: `);
|
||||||
output.log(`${chalk.cyan(name)} ${chalk.bold(type)} ${chalk.cyan(value)}`);
|
output.log(`${chalk.cyan(name)} ${chalk.bold(type)} ${chalk.cyan(value)}`);
|
||||||
return (await verifyData())
|
return (await verifyData(client))
|
||||||
? {
|
? {
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
value
|
value,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -85,13 +88,13 @@ export default async function getDNSData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyData() {
|
async function verifyData(client: Client) {
|
||||||
return promptBool('Is this correct?');
|
return promptBool('Is this correct?', client);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRecordName(type: string) {
|
async function getRecordName(type: string) {
|
||||||
const input = await textInput({
|
const input = await textInput({
|
||||||
label: `- ${type} name: `
|
label: `- ${type} name: `,
|
||||||
});
|
});
|
||||||
return input === '@' ? '' : input;
|
return input === '@' ? '' : input;
|
||||||
}
|
}
|
||||||
@@ -100,14 +103,14 @@ async function getNumber(label: string) {
|
|||||||
return Number(
|
return Number(
|
||||||
await textInput({
|
await textInput({
|
||||||
label,
|
label,
|
||||||
validateValue: v => Boolean(v && Number(v))
|
validateValue: v => Boolean(v && Number(v)),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
async function getTrimmedString(label: string) {
|
async function getTrimmedString(label: string) {
|
||||||
const res = await textInput({
|
const res = await textInput({
|
||||||
label,
|
label,
|
||||||
validateValue: v => Boolean(v && v.trim().length > 0)
|
validateValue: v => Boolean(v && v.trim().length > 0),
|
||||||
});
|
});
|
||||||
return res.trim();
|
return res.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export default async function purchaseDomainIfAvailable(
|
|||||||
!(await promptBool(
|
!(await promptBool(
|
||||||
`Buy ${chalk.underline(domain)} for ${chalk.bold(
|
`Buy ${chalk.underline(domain)} for ${chalk.bold(
|
||||||
`$${price}`
|
`$${price}`
|
||||||
)} (${plural('yr', period, true)})?`
|
)} (${plural('yr', period, true)})?`,
|
||||||
|
client
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
output.print(eraseLines(1));
|
output.print(eraseLines(1));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolve } from 'path';
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
import { resolve } from 'path';
|
||||||
import { getVercelIgnore } from '@vercel/client';
|
import { getVercelIgnore } from '@vercel/client';
|
||||||
import uniqueStrings from './unique-strings';
|
import uniqueStrings from './unique-strings';
|
||||||
import { Output } from './output/create-output';
|
import { Output } from './output/create-output';
|
||||||
|
|||||||
@@ -530,7 +530,7 @@ export default class Now extends EventEmitter {
|
|||||||
`${opts.method || 'GET'} ${this._apiUrl}${_url} ${opts.body || ''}`,
|
`${opts.method || 'GET'} ${this._apiUrl}${_url} ${opts.body || ''}`,
|
||||||
fetch(`${this._apiUrl}${_url}`, { ...opts, body })
|
fetch(`${this._apiUrl}${_url}`, { ...opts, body })
|
||||||
);
|
);
|
||||||
printIndications(res);
|
printIndications(this._client, res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import inquirer from 'inquirer';
|
import Client from '../client';
|
||||||
|
|
||||||
export default async function confirm(
|
export default async function confirm(
|
||||||
|
client: Client,
|
||||||
message: string,
|
message: string,
|
||||||
preferred: boolean
|
preferred: boolean
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
require('./patch-inquirer');
|
require('./patch-inquirer');
|
||||||
|
|
||||||
const answers = await inquirer.prompt({
|
const answers = await client.prompt({
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'value',
|
name: 'value',
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import confirm from './confirm';
|
import confirm from './confirm';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { Output } from '../output';
|
|
||||||
import frameworkList, { Framework } from '@vercel/frameworks';
|
import frameworkList, { Framework } from '@vercel/frameworks';
|
||||||
|
import Client from '../client';
|
||||||
import { isSettingValue } from '../is-setting-value';
|
import { isSettingValue } from '../is-setting-value';
|
||||||
import { ProjectSettings } from '../../types';
|
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 type PartialProjectSettings = Pick<ProjectSettings, ConfigKeys>;
|
||||||
|
|
||||||
export default async function editProjectSettings(
|
export default async function editProjectSettings(
|
||||||
output: Output,
|
client: Client,
|
||||||
projectSettings: PartialProjectSettings | null,
|
projectSettings: PartialProjectSettings | null,
|
||||||
framework: Framework | null,
|
framework: Framework | null,
|
||||||
autoConfirm: boolean,
|
autoConfirm: boolean,
|
||||||
localConfigurationOverrides: PartialProjectSettings | null
|
localConfigurationOverrides: PartialProjectSettings | null
|
||||||
): Promise<ProjectSettings> {
|
): Promise<ProjectSettings> {
|
||||||
|
const { output } = client;
|
||||||
|
|
||||||
// Create initial settings object defaulting everything to `null` and assigning what may exist in `projectSettings`
|
// Create initial settings object defaulting everything to `null` and assigning what may exist in `projectSettings`
|
||||||
const settings: ProjectSettings = Object.assign(
|
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.
|
// Prompt the user if they want to modify any settings not defined by local configuration.
|
||||||
if (
|
if (
|
||||||
autoConfirm ||
|
autoConfirm ||
|
||||||
!(await confirm('Want to modify these settings?', false))
|
!(await confirm(client, 'Want to modify these settings?', false))
|
||||||
) {
|
) {
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Client from '../client';
|
import Client from '../client';
|
||||||
import inquirer from 'inquirer';
|
|
||||||
import confirm from './confirm';
|
import confirm from './confirm';
|
||||||
import getProjectByIdOrName from '../projects/get-project-by-id-or-name';
|
import getProjectByIdOrName from '../projects/get-project-by-id-or-name';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
@@ -47,11 +46,16 @@ export default async function inputProject(
|
|||||||
|
|
||||||
if (!detectedProject) {
|
if (!detectedProject) {
|
||||||
// did not auto-detect a project to link
|
// 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 {
|
} else {
|
||||||
// auto-detected a project to link
|
// auto-detected a project to link
|
||||||
if (
|
if (
|
||||||
await confirm(
|
await confirm(
|
||||||
|
client,
|
||||||
`Found project ${chalk.cyan(
|
`Found project ${chalk.cyan(
|
||||||
`“${org.slug}/${detectedProject.name}”`
|
`“${org.slug}/${detectedProject.name}”`
|
||||||
)}. Link to it?`,
|
)}. Link to it?`,
|
||||||
@@ -63,6 +67,7 @@ export default async function inputProject(
|
|||||||
|
|
||||||
// user doesn't want to link the auto-detected project
|
// user doesn't want to link the auto-detected project
|
||||||
shouldLinkProject = await confirm(
|
shouldLinkProject = await confirm(
|
||||||
|
client,
|
||||||
`Link to different existing project?`,
|
`Link to different existing project?`,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -73,7 +78,7 @@ export default async function inputProject(
|
|||||||
let project: Project | ProjectNotFound | null = null;
|
let project: Project | ProjectNotFound | null = null;
|
||||||
|
|
||||||
while (!project || project instanceof ProjectNotFound) {
|
while (!project || project instanceof ProjectNotFound) {
|
||||||
const answers = await inquirer.prompt({
|
const answers = await client.prompt({
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'existingProjectName',
|
name: 'existingProjectName',
|
||||||
message: `What’s the name of your existing project?`,
|
message: `What’s the name of your existing project?`,
|
||||||
@@ -104,7 +109,7 @@ export default async function inputProject(
|
|||||||
let newProjectName: string | null = null;
|
let newProjectName: string | null = null;
|
||||||
|
|
||||||
while (!newProjectName) {
|
while (!newProjectName) {
|
||||||
const answers = await inquirer.prompt({
|
const answers = await client.prompt({
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'newProjectName',
|
name: 'newProjectName',
|
||||||
message: `What’s your project’s name?`,
|
message: `What’s your project’s name?`,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import inquirer from 'inquirer';
|
|
||||||
import { Output } from '../output';
|
|
||||||
import { validateRootDirectory } from '../validate-paths';
|
import { validateRootDirectory } from '../validate-paths';
|
||||||
|
import Client from '../client';
|
||||||
|
|
||||||
export async function inputRootDirectory(
|
export async function inputRootDirectory(
|
||||||
|
client: Client,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
output: Output,
|
|
||||||
autoConfirm = false
|
autoConfirm = false
|
||||||
) {
|
) {
|
||||||
if (autoConfirm) {
|
if (autoConfirm) {
|
||||||
@@ -15,7 +14,7 @@ export async function inputRootDirectory(
|
|||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
const { rootDirectory } = await inquirer.prompt({
|
const { rootDirectory } = await client.prompt({
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'rootDirectory',
|
name: 'rootDirectory',
|
||||||
message: `In which directory is your code located?`,
|
message: `In which directory is your code located?`,
|
||||||
@@ -38,7 +37,7 @@ export async function inputRootDirectory(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(await validateRootDirectory(
|
(await validateRootDirectory(
|
||||||
output,
|
client.output,
|
||||||
cwd,
|
cwd,
|
||||||
fullPath,
|
fullPath,
|
||||||
'Please choose a different one.'
|
'Please choose a different one.'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
|
import Client from '../client';
|
||||||
import eraseLines from '../output/erase-lines';
|
import eraseLines from '../output/erase-lines';
|
||||||
|
|
||||||
interface ListEntry {
|
interface ListEntry {
|
||||||
@@ -35,21 +36,24 @@ function getLength(input: string): number {
|
|||||||
return biggestLength;
|
return biggestLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function list({
|
export default async function list(
|
||||||
message = 'the question',
|
client: Client,
|
||||||
// eslint-disable-line no-unused-vars
|
{
|
||||||
choices: _choices = [
|
message = 'the question',
|
||||||
{
|
// eslint-disable-line no-unused-vars
|
||||||
name: 'something\ndescription\ndetails\netc',
|
choices: _choices = [
|
||||||
value: 'something unique',
|
{
|
||||||
short: 'generally the first line of `name`',
|
name: 'something\ndescription\ndetails\netc',
|
||||||
},
|
value: 'something unique',
|
||||||
],
|
short: 'generally the first line of `name`',
|
||||||
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
|
},
|
||||||
separator = false, // Puts a blank separator between each choice
|
],
|
||||||
abort = 'end', // Whether the `abort` option will be at the `start` or the `end`,
|
pageSize = 15, // Show 15 lines without scrolling (~4 credit cards)
|
||||||
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
|
separator = false, // Puts a blank separator between each choice
|
||||||
}: ListOptions): Promise<string> {
|
abort = 'end', // Whether the `abort` option will be at the `start` or the `end`,
|
||||||
|
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
|
||||||
|
}: ListOptions
|
||||||
|
): Promise<string> {
|
||||||
require('./patch-inquirer-legacy');
|
require('./patch-inquirer-legacy');
|
||||||
|
|
||||||
let biggestLength = 0;
|
let biggestLength = 0;
|
||||||
@@ -106,7 +110,7 @@ export default async function list({
|
|||||||
choices.push(abortSeparator, _abort);
|
choices.push(abortSeparator, _abort);
|
||||||
}
|
}
|
||||||
|
|
||||||
const answer = await inquirer.prompt({
|
const answer = await client.prompt({
|
||||||
name: 'value',
|
name: 'value',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
default: selected,
|
default: selected,
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import type { ReadableTTY, WritableTTY } from '../../types';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
abortSequences?: Set<string>;
|
abortSequences?: Set<string>;
|
||||||
defaultValue?: boolean;
|
defaultValue?: boolean;
|
||||||
noChar?: string;
|
noChar?: string;
|
||||||
resolveChars?: Set<string>;
|
resolveChars?: Set<string>;
|
||||||
stdin?: NodeJS.ReadStream;
|
stdin: ReadableTTY;
|
||||||
stdout?: NodeJS.WriteStream;
|
stdout: WritableTTY;
|
||||||
trailing?: string;
|
trailing?: string;
|
||||||
yesChar?: string;
|
yesChar?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function promptBool(label: string, options: Options = {}) {
|
export default async function promptBool(label: string, options: Options) {
|
||||||
const {
|
const {
|
||||||
|
stdin,
|
||||||
|
stdout,
|
||||||
defaultValue = false,
|
defaultValue = false,
|
||||||
abortSequences = new Set(['\u0003']),
|
abortSequences = new Set(['\u0003']),
|
||||||
resolveChars = new Set(['\r']),
|
resolveChars = new Set(['\r']),
|
||||||
yesChar = 'y',
|
yesChar = 'y',
|
||||||
noChar = 'n',
|
noChar = 'n',
|
||||||
stdin = process.stdin,
|
|
||||||
stdout = process.stdout,
|
|
||||||
trailing = '',
|
trailing = '',
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
export default async function readStandardInput(): Promise<string> {
|
import type { ReadableTTY } from '../../types';
|
||||||
|
|
||||||
|
export default async function readStandardInput(
|
||||||
|
stdin: ReadableTTY
|
||||||
|
): Promise<string> {
|
||||||
return new Promise<string>(resolve => {
|
return new Promise<string>(resolve => {
|
||||||
setTimeout(() => resolve(''), 500);
|
setTimeout(() => resolve(''), 500);
|
||||||
|
|
||||||
if (process.stdin.isTTY) {
|
if (stdin.isTTY) {
|
||||||
// found tty so we know there is nothing piped to stdin
|
// found tty so we know there is nothing piped to stdin
|
||||||
resolve('');
|
resolve('');
|
||||||
} else {
|
} else {
|
||||||
process.stdin.setEncoding('utf8');
|
stdin.setEncoding('utf8');
|
||||||
process.stdin.once('data', resolve);
|
stdin.once('data', resolve);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default async function setupAndLink(
|
|||||||
return { status: 'error', exitCode: 1, reason: 'PATH_IS_FILE' };
|
return { status: 'error', exitCode: 1, reason: 'PATH_IS_FILE' };
|
||||||
}
|
}
|
||||||
const link = await getLinkedProject(client, path);
|
const link = await getLinkedProject(client, path);
|
||||||
const isTTY = process.stdout.isTTY;
|
const isTTY = client.stdin.isTTY;
|
||||||
const quiet = !isTTY;
|
const quiet = !isTTY;
|
||||||
let rootDirectory: string | null = null;
|
let rootDirectory: string | null = null;
|
||||||
let sourceFilesOutsideRootDirectory = true;
|
let sourceFilesOutsideRootDirectory = true;
|
||||||
@@ -80,6 +80,7 @@ export default async function setupAndLink(
|
|||||||
const shouldStartSetup =
|
const shouldStartSetup =
|
||||||
autoConfirm ||
|
autoConfirm ||
|
||||||
(await confirm(
|
(await confirm(
|
||||||
|
client,
|
||||||
`${setupMsg} ${chalk.cyan(`“${toHumanPath(path)}”`)}?`,
|
`${setupMsg} ${chalk.cyan(`“${toHumanPath(path)}”`)}?`,
|
||||||
true
|
true
|
||||||
));
|
));
|
||||||
@@ -120,7 +121,7 @@ export default async function setupAndLink(
|
|||||||
|
|
||||||
if (typeof projectOrNewProjectName === 'string') {
|
if (typeof projectOrNewProjectName === 'string') {
|
||||||
newProjectName = projectOrNewProjectName;
|
newProjectName = projectOrNewProjectName;
|
||||||
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
|
rootDirectory = await inputRootDirectory(client, path, autoConfirm);
|
||||||
} else {
|
} else {
|
||||||
const project = projectOrNewProjectName;
|
const project = projectOrNewProjectName;
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ export default async function setupAndLink(
|
|||||||
const { projectSettings, framework } = deployment;
|
const { projectSettings, framework } = deployment;
|
||||||
|
|
||||||
settings = await editProjectSettings(
|
settings = await editProjectSettings(
|
||||||
output,
|
client,
|
||||||
projectSettings,
|
projectSettings,
|
||||||
framework,
|
framework,
|
||||||
autoConfirm,
|
autoConfirm,
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ async function getVerificationTokenOutOfBand(client: Client, url: URL) {
|
|||||||
output.log(
|
output.log(
|
||||||
`After login is complete, enter the verification code printed in your browser.`
|
`After login is complete, enter the verification code printed in your browser.`
|
||||||
);
|
);
|
||||||
const verificationToken = await readInput('Verification code:');
|
const verificationToken = await readInput(client, 'Verification code:');
|
||||||
output.print(eraseLines(6));
|
output.print(eraseLines(6));
|
||||||
|
|
||||||
// If the pasted token begins with "saml_", then the `ssoUserId` was returned.
|
// If the pasted token begins with "saml_", then the `ssoUserId` was returned.
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import inquirer from 'inquirer';
|
|
||||||
import Client from '../client';
|
import Client from '../client';
|
||||||
import error from '../output/error';
|
import error from '../output/error';
|
||||||
import listInput from '../input/list';
|
import listInput from '../input/list';
|
||||||
@@ -32,7 +31,7 @@ export default async function prompt(
|
|||||||
choices.pop();
|
choices.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const choice = await listInput({
|
const choice = await listInput(client, {
|
||||||
message: 'Log in to Vercel',
|
message: 'Log in to Vercel',
|
||||||
choices,
|
choices,
|
||||||
});
|
});
|
||||||
@@ -44,22 +43,26 @@ export default async function prompt(
|
|||||||
} else if (choice === 'bitbucket') {
|
} else if (choice === 'bitbucket') {
|
||||||
result = await doBitbucketLogin(client, outOfBand, ssoUserId);
|
result = await doBitbucketLogin(client, outOfBand, ssoUserId);
|
||||||
} else if (choice === 'email') {
|
} else if (choice === 'email') {
|
||||||
const email = await readInput('Enter your email address:');
|
const email = await readInput(client, 'Enter your email address:');
|
||||||
result = await doEmailLogin(client, email, ssoUserId);
|
result = await doEmailLogin(client, email, ssoUserId);
|
||||||
} else if (choice === 'saml') {
|
} else if (choice === 'saml') {
|
||||||
const slug = error?.teamId || (await readInput('Enter your Team slug:'));
|
const slug =
|
||||||
|
error?.teamId || (await readInput(client, 'Enter your Team slug:'));
|
||||||
result = await doSamlLogin(client, slug, outOfBand, ssoUserId);
|
result = await doSamlLogin(client, slug, outOfBand, ssoUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readInput(message: string): Promise<string> {
|
export async function readInput(
|
||||||
|
client: Client,
|
||||||
|
message: string
|
||||||
|
): Promise<string> {
|
||||||
let input;
|
let input;
|
||||||
|
|
||||||
while (!input) {
|
while (!input) {
|
||||||
try {
|
try {
|
||||||
const { val } = await inquirer.prompt({
|
const { val } = await client.prompt({
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'val',
|
name: 'val',
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default async function reauthenticate(
|
|||||||
client.output.log(
|
client.output.log(
|
||||||
`You must re-authenticate with SAML to use ${bold(error.scope)} scope.`
|
`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);
|
return doSamlLogin(client, error.teamId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,41 +1,39 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import renderLink from './link';
|
import renderLink from './link';
|
||||||
import wait, { StopSpinner } from './wait';
|
import wait, { StopSpinner } from './wait';
|
||||||
import { Writable } from 'stream';
|
import type { WritableTTY } from '../../types';
|
||||||
|
|
||||||
export interface OutputOptions {
|
export interface OutputOptions {
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrintOptions {
|
export interface LogOptions {
|
||||||
w?: Writable;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LogOptions extends PrintOptions {
|
|
||||||
color?: typeof chalk;
|
color?: typeof chalk;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Output {
|
export class Output {
|
||||||
|
stream: WritableTTY;
|
||||||
debugEnabled: boolean;
|
debugEnabled: boolean;
|
||||||
private spinnerMessage: string;
|
private spinnerMessage: string;
|
||||||
private _spinner: StopSpinner | null;
|
private _spinner: StopSpinner | null;
|
||||||
isTTY: boolean;
|
|
||||||
|
|
||||||
constructor({ debug: debugEnabled = false }: OutputOptions = {}) {
|
constructor(
|
||||||
|
stream: WritableTTY,
|
||||||
|
{ debug: debugEnabled = false }: OutputOptions = {}
|
||||||
|
) {
|
||||||
|
this.stream = stream;
|
||||||
this.debugEnabled = debugEnabled;
|
this.debugEnabled = debugEnabled;
|
||||||
this.spinnerMessage = '';
|
this.spinnerMessage = '';
|
||||||
this._spinner = null;
|
this._spinner = null;
|
||||||
this.isTTY = process.stdout.isTTY || false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isDebugEnabled = () => {
|
isDebugEnabled = () => {
|
||||||
return this.debugEnabled;
|
return this.debugEnabled;
|
||||||
};
|
};
|
||||||
|
|
||||||
print = (str: string, { w }: PrintOptions = { w: process.stderr }) => {
|
print = (str: string) => {
|
||||||
this.stopSpinner();
|
this.stopSpinner();
|
||||||
const stream: Writable = w || process.stderr;
|
this.stream.write(str);
|
||||||
stream.write(str);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
log = (str: string, color = chalk.grey) => {
|
log = (str: string, color = chalk.grey) => {
|
||||||
@@ -111,11 +109,17 @@ export class Output {
|
|||||||
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
|
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.isTTY) {
|
if (this.stream.isTTY) {
|
||||||
if (this._spinner) {
|
if (this._spinner) {
|
||||||
this._spinner.text = message;
|
this._spinner.text = message;
|
||||||
} else {
|
} else {
|
||||||
this._spinner = wait(message, delay);
|
this._spinner = wait(
|
||||||
|
{
|
||||||
|
text: message,
|
||||||
|
stream: this.stream,
|
||||||
|
},
|
||||||
|
delay
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.print(`${message}\n`);
|
this.print(`${message}\n`);
|
||||||
@@ -157,7 +161,3 @@ export class Output {
|
|||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createOutput(opts?: OutputOptions) {
|
|
||||||
return new Output(opts);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { default, Output } from './create-output';
|
export { Output } from './create-output';
|
||||||
export { StopSpinner } from './wait';
|
export { StopSpinner } from './wait';
|
||||||
|
|||||||
@@ -8,14 +8,19 @@ export interface StopSpinner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function wait(
|
export default function wait(
|
||||||
msg: string,
|
opts: ora.Options,
|
||||||
delay: number = 300,
|
delay: number = 300
|
||||||
_ora = ora
|
|
||||||
): StopSpinner {
|
): StopSpinner {
|
||||||
let spinner: ReturnType<typeof _ora> | null = null;
|
let text = opts.text;
|
||||||
|
let spinner: ora.Ora | null = null;
|
||||||
|
|
||||||
|
if (typeof text !== 'string') {
|
||||||
|
throw new Error(`"text" is required for Ora spinner`);
|
||||||
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
spinner = _ora(chalk.gray(msg));
|
spinner = ora(opts);
|
||||||
|
spinner.text = chalk.gray(text);
|
||||||
spinner.color = 'gray';
|
spinner.color = 'gray';
|
||||||
spinner.start();
|
spinner.start();
|
||||||
}, delay);
|
}, delay);
|
||||||
@@ -29,23 +34,21 @@ export default function wait(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stop.text = msg;
|
stop.text = text;
|
||||||
|
|
||||||
// Allow `text` property to update the text while the spinner is in action
|
// Allow `text` property to update the text while the spinner is in action
|
||||||
Object.defineProperty(stop, 'text', {
|
Object.defineProperty(stop, 'text', {
|
||||||
get() {
|
get() {
|
||||||
return msg;
|
return text;
|
||||||
},
|
},
|
||||||
|
|
||||||
set(v: string) {
|
set(v: string) {
|
||||||
msg = v;
|
text = v;
|
||||||
if (spinner) {
|
if (spinner) {
|
||||||
spinner.text = chalk.gray(v);
|
spinner.text = chalk.gray(v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
process.once('nowExit', stop);
|
|
||||||
return stop;
|
return stop;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { Response } from 'node-fetch';
|
import { Response } from 'node-fetch';
|
||||||
import { emoji, EmojiLabel, prependEmoji } from './emoji';
|
import Client from './client';
|
||||||
import createOutput from './output';
|
|
||||||
import linkStyle from './output/link';
|
import linkStyle from './output/link';
|
||||||
|
import { emoji, EmojiLabel, prependEmoji } from './emoji';
|
||||||
|
|
||||||
export default function printIndications(res: Response) {
|
export default function printIndications(client: Client, res: Response) {
|
||||||
const _output = createOutput();
|
|
||||||
const indications = new Set(['warning', 'notice', 'tip']);
|
const indications = new Set(['warning', 'notice', 'tip']);
|
||||||
const regex = /^x-(?:vercel|now)-(warning|notice|tip)-(.*)$/;
|
const regex = /^x-(?:vercel|now)-(warning|notice|tip)-(.*)$/;
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ export default function printIndications(res: Response) {
|
|||||||
chalk.dim(`${action || 'Learn More'}: ${linkStyle(link)}`) +
|
chalk.dim(`${action || 'Learn More'}: ${linkStyle(link)}`) +
|
||||||
newline;
|
newline;
|
||||||
}
|
}
|
||||||
_output.print(message + finalLink);
|
client.output.print(message + finalLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import confirm from './input/confirm';
|
import confirm from './input/confirm';
|
||||||
import toHumanPath from './humanize-path';
|
import toHumanPath from './humanize-path';
|
||||||
|
import Client from './client';
|
||||||
|
|
||||||
const stat = promisify(lstatRaw);
|
const stat = promisify(lstatRaw);
|
||||||
|
|
||||||
@@ -51,9 +52,11 @@ export async function validateRootDirectory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function validatePaths(
|
export default async function validatePaths(
|
||||||
output: Output,
|
client: Client,
|
||||||
paths: string[]
|
paths: string[]
|
||||||
): Promise<{ valid: true; path: string } | { valid: false; exitCode: number }> {
|
): Promise<{ valid: true; path: string } | { valid: false; exitCode: number }> {
|
||||||
|
const { output } = client;
|
||||||
|
|
||||||
// can't deploy more than 1 path
|
// can't deploy more than 1 path
|
||||||
if (paths.length > 1) {
|
if (paths.length > 1) {
|
||||||
output.print(`${chalk.red('Error!')} Can't deploy more than one path.\n`);
|
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
|
// ask confirmation if the directory is home
|
||||||
if (path === homedir()) {
|
if (path === homedir()) {
|
||||||
const shouldDeployHomeDirectory = await confirm(
|
const shouldDeployHomeDirectory = await confirm(
|
||||||
|
client,
|
||||||
`You are deploying your home directory. Do you want to continue?`,
|
`You are deploying your home directory. Do you want to continue?`,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export const config = {
|
|||||||
runtime: 'experimental-edge'
|
runtime: 'experimental-edge'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function edge(request: Request, event: Event) {
|
export default async function edge(request, event) {
|
||||||
return new Response('some response body');
|
return new Response('some response body');
|
||||||
|
|
||||||
// intentional missing closing bracket to produce syntax error
|
// intentional missing closing bracket to produce syntax error
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ export const config = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function middleware(request, _event) {
|
export default function middleware(request, _event) {
|
||||||
const response = new Response('middleware response');
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const response = new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
pathname: url.pathname,
|
||||||
|
search: url.search,
|
||||||
|
fromMiddleware: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Set custom header
|
// Set custom header
|
||||||
response.headers.set('x-modified-edge', 'true');
|
response.headers.set('x-modified-edge', 'true');
|
||||||
|
|||||||
3
packages/cli/test/dev/fixtures/no-api/vercel.json
Normal file
3
packages/cli/test/dev/fixtures/no-api/vercel.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"version": 2
|
||||||
|
}
|
||||||
@@ -161,14 +161,13 @@ test('[vercel dev] should handle startup errors thrown in edge functions', async
|
|||||||
});
|
});
|
||||||
validateResponseHeaders(res);
|
validateResponseHeaders(res);
|
||||||
|
|
||||||
const { stdout, stderr } = await dev.kill('SIGTERM');
|
const { stderr } = await dev.kill('SIGTERM');
|
||||||
|
|
||||||
expect(await res.text()).toMatch(
|
expect(await res.text()).toMatch(
|
||||||
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
||||||
);
|
);
|
||||||
expect(stdout).toMatch(
|
expect(stderr).toMatch(/Failed to instantiate edge runtime./g);
|
||||||
/Failed to instantiate edge runtime: intentional startup error/g
|
expect(stderr).toMatch(/intentional startup error/g);
|
||||||
);
|
|
||||||
expect(stderr).toMatch(
|
expect(stderr).toMatch(
|
||||||
/Failed to complete request to \/api\/edge-error-startup: Error: socket hang up/g
|
/Failed to complete request to \/api\/edge-error-startup: Error: socket hang up/g
|
||||||
);
|
);
|
||||||
@@ -193,14 +192,13 @@ test('[vercel dev] should handle syntax errors thrown in edge functions', async
|
|||||||
});
|
});
|
||||||
validateResponseHeaders(res);
|
validateResponseHeaders(res);
|
||||||
|
|
||||||
const { stdout, stderr } = await dev.kill('SIGTERM');
|
const { stderr } = await dev.kill('SIGTERM');
|
||||||
|
|
||||||
expect(await res.text()).toMatch(
|
expect(await res.text()).toMatch(
|
||||||
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
||||||
);
|
);
|
||||||
expect(stdout).toMatch(
|
expect(stderr).toMatch(/Failed to instantiate edge runtime./g);
|
||||||
/Failed to instantiate edge runtime: Module parse failed: Unexpected token/g
|
expect(stderr).toMatch(/Unexpected end of file/g);
|
||||||
);
|
|
||||||
expect(stderr).toMatch(
|
expect(stderr).toMatch(
|
||||||
/Failed to complete request to \/api\/edge-error-syntax: Error: socket hang up/g
|
/Failed to complete request to \/api\/edge-error-syntax: Error: socket hang up/g
|
||||||
);
|
);
|
||||||
@@ -228,13 +226,13 @@ test('[vercel dev] should handle import errors thrown in edge functions', async
|
|||||||
);
|
);
|
||||||
validateResponseHeaders(res);
|
validateResponseHeaders(res);
|
||||||
|
|
||||||
const { stdout, stderr } = await dev.kill('SIGTERM');
|
const { stderr } = await dev.kill('SIGTERM');
|
||||||
|
|
||||||
expect(await res.text()).toMatch(
|
expect(await res.text()).toMatch(
|
||||||
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
/<strong>500<\/strong>: INTERNAL_SERVER_ERROR/g
|
||||||
);
|
);
|
||||||
expect(stdout).toMatch(
|
expect(stderr).toMatch(
|
||||||
/Failed to instantiate edge runtime: Code generation from strings disallowed for this context/g
|
/Could not resolve "unknown-module-893427589372458934795843"/g
|
||||||
);
|
);
|
||||||
expect(stderr).toMatch(
|
expect(stderr).toMatch(
|
||||||
/Failed to complete request to \/api\/edge-error-unknown-import: Error: socket hang up/g
|
/Failed to complete request to \/api\/edge-error-unknown-import: Error: socket hang up/g
|
||||||
@@ -244,7 +242,7 @@ test('[vercel dev] should handle import errors thrown in edge functions', async
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[vercel dev] should handle import errors thrown in edge functions', async () => {
|
test('[vercel dev] should handle missing handler errors thrown in edge functions', async () => {
|
||||||
const dir = fixture('edge-function-error');
|
const dir = fixture('edge-function-error');
|
||||||
const { dev, port, readyResolver } = await testFixture(dir);
|
const { dev, port, readyResolver } = await testFixture(dir);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
// eslint-disable-next-line
|
// 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 () => {
|
test('[vercel dev] validate redirects', async () => {
|
||||||
const directory = fixture('invalid-redirects');
|
const directory = fixture('invalid-redirects');
|
||||||
@@ -334,3 +344,44 @@ test(
|
|||||||
await testPath(200, '/', /A simple deployment with the Vercel API!/m);
|
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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -508,18 +508,23 @@ test(
|
|||||||
|
|
||||||
test(
|
test(
|
||||||
'[vercel dev] Middleware with `matchers` config',
|
'[vercel dev] Middleware with `matchers` config',
|
||||||
testFixtureStdio(
|
testFixtureStdio('middleware-matchers', async (testPath: any) => {
|
||||||
'middleware-matchers',
|
await testPath(404, '/');
|
||||||
async (testPath: any) => {
|
await testPath(404, '/another');
|
||||||
// TODO: remove once latest `@vercel/node` is shipped to stable with `matchers` support (fails because `directoryListing`)
|
await testPath(
|
||||||
//await testPath(404, '/');
|
200,
|
||||||
await testPath(404, '/another');
|
'/about/page',
|
||||||
await testPath(200, '/about/page', 'middleware response');
|
'{"pathname":"/about/page","search":"","fromMiddleware":true}'
|
||||||
await testPath(200, '/dashboard/home', 'middleware response');
|
);
|
||||||
},
|
await testPath(
|
||||||
{
|
200,
|
||||||
// TODO: remove once latest `@vercel/node` is shipped to stable with `matchers` support
|
'/dashboard/home',
|
||||||
skipDeploy: true,
|
'{"pathname":"/dashboard/home","search":"","fromMiddleware":true}'
|
||||||
}
|
);
|
||||||
)
|
await testPath(
|
||||||
|
200,
|
||||||
|
'/dashboard/home?a=b',
|
||||||
|
'{"pathname":"/dashboard/home","search":"?a=b","fromMiddleware":true}'
|
||||||
|
);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
2
packages/cli/test/integration.js
vendored
2
packages/cli/test/integration.js
vendored
@@ -1958,7 +1958,7 @@ test('ensure we render a prompt when deploying home directory', async t => {
|
|||||||
t.is(exitCode, 0);
|
t.is(exitCode, 0);
|
||||||
|
|
||||||
t.true(
|
t.true(
|
||||||
stdout.includes(
|
stderr.includes(
|
||||||
'You are deploying your home directory. Do you want to continue? [y/N]'
|
'You are deploying your home directory. Do you want to continue? [y/N]'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
// Register Jest matcher extensions for CLI unit tests
|
||||||
|
import './matchers';
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { PassThrough } from 'stream';
|
||||||
import { createServer, Server } from 'http';
|
import { createServer, Server } from 'http';
|
||||||
import express, { Express, Router } from 'express';
|
import express, { Express, Router } from 'express';
|
||||||
import listen from 'async-listen';
|
import listen from 'async-listen';
|
||||||
@@ -11,23 +15,42 @@ chalk.level = 0;
|
|||||||
|
|
||||||
export type Scenario = Router;
|
export type Scenario = Router;
|
||||||
|
|
||||||
|
class MockStream extends PassThrough {
|
||||||
|
isTTY: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.isTTY = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These is for the `ora` module
|
||||||
|
clearLine() {}
|
||||||
|
cursorTo() {}
|
||||||
|
}
|
||||||
|
|
||||||
export class MockClient extends Client {
|
export class MockClient extends Client {
|
||||||
mockServer?: Server;
|
stdin!: MockStream;
|
||||||
mockOutput: jest.Mock<void, Parameters<Output['print']>>;
|
stdout!: MockStream;
|
||||||
private app: Express;
|
stderr!: MockStream;
|
||||||
scenario: Scenario;
|
scenario: Scenario;
|
||||||
|
mockServer?: Server;
|
||||||
|
private app: Express;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
argv: [],
|
|
||||||
// Gets populated in `startMockServer()`
|
// Gets populated in `startMockServer()`
|
||||||
apiUrl: '',
|
apiUrl: '',
|
||||||
|
|
||||||
|
// Gets re-initialized for every test in `reset()`
|
||||||
|
argv: [],
|
||||||
authConfig: {},
|
authConfig: {},
|
||||||
output: new Output(),
|
|
||||||
config: {},
|
config: {},
|
||||||
localConfig: {},
|
localConfig: {},
|
||||||
|
stdin: new PassThrough(),
|
||||||
|
stdout: new PassThrough(),
|
||||||
|
stderr: new PassThrough(),
|
||||||
|
output: new Output(new PassThrough()),
|
||||||
});
|
});
|
||||||
this.mockOutput = jest.fn();
|
|
||||||
|
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.app.use(express.json());
|
this.app.use(express.json());
|
||||||
@@ -53,27 +76,29 @@ export class MockClient extends Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.output = new Output();
|
this.stdin = new MockStream();
|
||||||
this.mockOutput = jest.fn();
|
|
||||||
this.output.print = s => {
|
this.stdout = new MockStream();
|
||||||
return this.mockOutput(s);
|
this.stdout.setEncoding('utf8');
|
||||||
};
|
this.stdout.end = () => {};
|
||||||
|
this.stdout.pause();
|
||||||
|
|
||||||
|
this.stderr = new MockStream();
|
||||||
|
this.stderr.setEncoding('utf8');
|
||||||
|
this.stderr.end = () => {};
|
||||||
|
this.stderr.pause();
|
||||||
|
this.stderr.isTTY = true;
|
||||||
|
|
||||||
|
this._createPromptModule();
|
||||||
|
|
||||||
|
this.output = new Output(this.stderr);
|
||||||
|
|
||||||
this.argv = [];
|
this.argv = [];
|
||||||
this.authConfig = {};
|
this.authConfig = {};
|
||||||
this.config = {};
|
this.config = {};
|
||||||
this.localConfig = {};
|
this.localConfig = {};
|
||||||
|
|
||||||
// Just make this one silent
|
|
||||||
this.output.spinner = () => {};
|
|
||||||
|
|
||||||
this.scenario = Router();
|
this.scenario = Router();
|
||||||
|
|
||||||
this.output.isTTY = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get outputBuffer() {
|
|
||||||
return this.mockOutput.mock.calls.map(c => c[0]).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async startMockServer() {
|
async startMockServer() {
|
||||||
|
|||||||
65
packages/cli/test/mocks/matchers/index.ts
Normal file
65
packages/cli/test/mocks/matchers/index.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* This file registers the custom Jest "matchers" that are useful for
|
||||||
|
* writing CLI unit tests, and sets them up to be recognized by TypeScript.
|
||||||
|
*
|
||||||
|
* References:
|
||||||
|
* - https://haspar.us/notes/adding-jest-custom-matchers-in-typescript
|
||||||
|
* - https://gist.github.com/hasparus/4ebaa17ec5d3d44607f522bcb1cda9fb
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="@types/jest" />
|
||||||
|
|
||||||
|
import * as matchers from './matchers';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
type Tail<T extends unknown[]> = T extends [infer _Head, ...infer Tail]
|
||||||
|
? Tail
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type AnyFunction = (...args: any[]) => any;
|
||||||
|
type PromiseFunction = (...args: any[]) => Promise<any>;
|
||||||
|
|
||||||
|
type GetMatcherType<TP, TResult> = TP extends PromiseFunction
|
||||||
|
? (...args: Tail<Parameters<TP>>) => Promise<TResult>
|
||||||
|
: TP extends AnyFunction
|
||||||
|
? (...args: Tail<Parameters<TP>>) => TResult
|
||||||
|
: TP;
|
||||||
|
|
||||||
|
//type T = GetMatcherType<typeof matchers['toOutput'], void>;
|
||||||
|
|
||||||
|
type GetMatchersType<TMatchers, TResult> = {
|
||||||
|
[P in keyof TMatchers]: GetMatcherType<TMatchers[P], TResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FirstParam<T extends AnyFunction> = Parameters<T>[0];
|
||||||
|
|
||||||
|
type OnlyMethodsWhereFirstArgIsOfType<TObject, TWantedFirstArg> = {
|
||||||
|
[P in keyof TObject]: TObject[P] extends AnyFunction
|
||||||
|
? TWantedFirstArg extends FirstParam<TObject[P]>
|
||||||
|
? TObject[P]
|
||||||
|
: [
|
||||||
|
`Error: this function is present only when received is:`,
|
||||||
|
FirstParam<TObject[P]>
|
||||||
|
]
|
||||||
|
: TObject[P];
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace jest {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface Matchers<R, T = {}>
|
||||||
|
extends GetMatchersType<
|
||||||
|
OnlyMethodsWhereFirstArgIsOfType<typeof matchers, T>,
|
||||||
|
R
|
||||||
|
> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jestExpect = (global as any).expect;
|
||||||
|
|
||||||
|
if (jestExpect !== undefined) {
|
||||||
|
jestExpect.extend(matchers);
|
||||||
|
} else {
|
||||||
|
console.error("Couldn't find Jest's global expect.");
|
||||||
|
}
|
||||||
1
packages/cli/test/mocks/matchers/matchers.ts
Normal file
1
packages/cli/test/mocks/matchers/matchers.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './to-output';
|
||||||
67
packages/cli/test/mocks/matchers/to-output.ts
Normal file
67
packages/cli/test/mocks/matchers/to-output.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
getLabelPrinter,
|
||||||
|
matcherHint,
|
||||||
|
printExpected,
|
||||||
|
printReceived,
|
||||||
|
} from 'jest-matcher-utils';
|
||||||
|
import type { Readable } from 'stream';
|
||||||
|
import type { MatcherState } from 'expect';
|
||||||
|
import type { MatcherHintOptions } from 'jest-matcher-utils';
|
||||||
|
|
||||||
|
export async function toOutput(
|
||||||
|
this: MatcherState,
|
||||||
|
stream: Readable,
|
||||||
|
test: string,
|
||||||
|
timeout = 3000
|
||||||
|
) {
|
||||||
|
const { isNot } = this;
|
||||||
|
const matcherName = 'toOutput';
|
||||||
|
const matcherHintOptions: MatcherHintOptions = {
|
||||||
|
isNot,
|
||||||
|
promise: this.promise,
|
||||||
|
};
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let output = '';
|
||||||
|
let timeoutId = setTimeout(onTimeout, timeout);
|
||||||
|
|
||||||
|
const message = () => {
|
||||||
|
const labelExpected = 'Expected output';
|
||||||
|
const labelReceived = 'Received output';
|
||||||
|
const printLabel = getLabelPrinter(labelExpected, labelReceived);
|
||||||
|
const hint =
|
||||||
|
matcherHint(matcherName, 'stream', 'test', matcherHintOptions) + '\n\n';
|
||||||
|
return (
|
||||||
|
hint +
|
||||||
|
printLabel(labelExpected) +
|
||||||
|
(isNot ? 'not ' : '') +
|
||||||
|
printExpected(test) +
|
||||||
|
'\n' +
|
||||||
|
printLabel(labelReceived) +
|
||||||
|
(isNot ? ' ' : '') +
|
||||||
|
printReceived(output)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function onData(data: string) {
|
||||||
|
output += data;
|
||||||
|
if (output.includes(test)) {
|
||||||
|
cleanup();
|
||||||
|
resolve({ pass: true, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTimeout() {
|
||||||
|
cleanup();
|
||||||
|
resolve({ pass: false, message });
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
stream.removeListener('data', onData);
|
||||||
|
stream.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on('data', onData);
|
||||||
|
stream.resume();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -10,38 +10,38 @@ import { useUser } from '../../mocks/user';
|
|||||||
describe('deploy', () => {
|
describe('deploy', () => {
|
||||||
it('should reject deploying a single file', async () => {
|
it('should reject deploying a single file', async () => {
|
||||||
client.setArgv('deploy', __filename);
|
client.setArgv('deploy', __filename);
|
||||||
const exitCode = await deploy(client);
|
const exitCodePromise = deploy(client);
|
||||||
expect(exitCode).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
`Error! Support for single file deployments has been removed.\nLearn More: https://vercel.link/no-single-file-deployments\n`
|
`Error! Support for single file deployments has been removed.\nLearn More: https://vercel.link/no-single-file-deployments\n`
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject deploying multiple files', async () => {
|
it('should reject deploying multiple files', async () => {
|
||||||
client.setArgv('deploy', __filename, join(__dirname, 'inspect.test.ts'));
|
client.setArgv('deploy', __filename, join(__dirname, 'inspect.test.ts'));
|
||||||
const exitCode = await deploy(client);
|
const exitCodePromise = deploy(client);
|
||||||
expect(exitCode).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
`Error! Can't deploy more than one path.\n`
|
`Error! Can't deploy more than one path.\n`
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject deploying a directory that does not exist', async () => {
|
it('should reject deploying a directory that does not exist', async () => {
|
||||||
client.setArgv('deploy', 'does-not-exists');
|
client.setArgv('deploy', 'does-not-exists');
|
||||||
const exitCode = await deploy(client);
|
const exitCodePromise = deploy(client);
|
||||||
expect(exitCode).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
`Error! The specified file or directory "does-not-exists" does not exist.\n`
|
`Error! The specified file or directory "does-not-exists" does not exist.\n`
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject deploying a directory that does not contain ".vercel/output" when `--prebuilt` is used', async () => {
|
it('should reject deploying a directory that does not contain ".vercel/output" when `--prebuilt` is used', async () => {
|
||||||
client.setArgv('deploy', __dirname, '--prebuilt');
|
client.setArgv('deploy', __dirname, '--prebuilt');
|
||||||
const exitCode = await deploy(client);
|
const exitCodePromise = deploy(client);
|
||||||
expect(exitCode).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
'Error! The "--prebuilt" option was used, but no prebuilt output found in ".vercel/output". Run `vercel build` to generate a local build.\n'
|
'Error! The "--prebuilt" option was used, but no prebuilt output found in ".vercel/output". Run `vercel build` to generate a local build.\n'
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject deploying a directory that was built with a different target environment when `--prebuilt --prod` is used on "preview" output', async () => {
|
it('should reject deploying a directory that was built with a different target environment when `--prebuilt --prod` is used on "preview" output', async () => {
|
||||||
@@ -56,14 +56,14 @@ describe('deploy', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.setArgv('deploy', cwd, '--prebuilt', '--prod');
|
client.setArgv('deploy', cwd, '--prebuilt', '--prod');
|
||||||
const exitCode = await deploy(client);
|
const exitCodePromise = deploy(client);
|
||||||
expect(exitCode).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
'Error! The "--prebuilt" option was used with the target environment "production",' +
|
'Error! The "--prebuilt" option was used with the target environment "production",' +
|
||||||
' but the prebuilt output found in ".vercel/output" was built with target environment "preview".' +
|
' but the prebuilt output found in ".vercel/output" was built with target environment "preview".' +
|
||||||
' Please run `vercel --prebuilt`.\n' +
|
' Please run `vercel --prebuilt`.\n' +
|
||||||
'Learn More: https://vercel.link/prebuilt-environment-mismatch\n'
|
'Learn More: https://vercel.link/prebuilt-environment-mismatch\n'
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject deploying a directory that was built with a different target environment when `--prebuilt` is used on "production" output', async () => {
|
it('should reject deploying a directory that was built with a different target environment when `--prebuilt` is used on "production" output', async () => {
|
||||||
@@ -78,14 +78,14 @@ describe('deploy', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.setArgv('deploy', cwd, '--prebuilt');
|
client.setArgv('deploy', cwd, '--prebuilt');
|
||||||
const exitCode = await deploy(client);
|
const exitCodePromise = deploy(client);
|
||||||
expect(exitCode).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
'Error! The "--prebuilt" option was used with the target environment "preview",' +
|
'Error! The "--prebuilt" option was used with the target environment "preview",' +
|
||||||
' but the prebuilt output found in ".vercel/output" was built with target environment "production".' +
|
' but the prebuilt output found in ".vercel/output" was built with target environment "production".' +
|
||||||
' Please run `vercel --prebuilt --prod`.\n' +
|
' Please run `vercel --prebuilt --prod`.\n' +
|
||||||
'Learn More: https://vercel.link/prebuilt-environment-mismatch\n'
|
'Learn More: https://vercel.link/prebuilt-environment-mismatch\n'
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject deploying "version: 1"', async () => {
|
it('should reject deploying "version: 1"', async () => {
|
||||||
@@ -94,11 +94,11 @@ describe('deploy', () => {
|
|||||||
[fileNameSymbol]: 'vercel.json',
|
[fileNameSymbol]: 'vercel.json',
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
};
|
||||||
const exitCode = await deploy(client);
|
const exitCodePromise = deploy(client);
|
||||||
expect(exitCode).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
'Error! The value of the `version` property within vercel.json can only be `2`.\n'
|
'Error! The value of the `version` property within vercel.json can only be `2`.\n'
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject deploying "version: {}"', async () => {
|
it('should reject deploying "version: {}"', async () => {
|
||||||
@@ -108,10 +108,10 @@ describe('deploy', () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
version: {},
|
version: {},
|
||||||
};
|
};
|
||||||
const exitCode = await deploy(client);
|
const exitCodePromise = deploy(client);
|
||||||
expect(exitCode).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
'Error! The `version` property inside your vercel.json file must be a number.\n'
|
'Error! The `version` property inside your vercel.json file must be a number.\n'
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ describe('env', () => {
|
|||||||
name: 'vercel-env-pull',
|
name: 'vercel-env-pull',
|
||||||
});
|
});
|
||||||
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
|
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
|
||||||
const exitCode = await env(client);
|
const exitCodePromise = env(client);
|
||||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
await expect(client.stderr).toOutput(
|
||||||
|
'Downloading "development" Environment Variables for Project vercel-env-pull'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Created .env file');
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
|
||||||
const rawDevEnv = await fs.readFile(path.join(cwd, '.env'));
|
const rawDevEnv = await fs.readFile(path.join(cwd, '.env'));
|
||||||
|
|
||||||
@@ -39,8 +43,12 @@ describe('env', () => {
|
|||||||
name: 'vercel-env-pull',
|
name: 'vercel-env-pull',
|
||||||
});
|
});
|
||||||
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
|
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
|
||||||
const exitCode = await env(client);
|
const exitCodePromise = env(client);
|
||||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
await expect(client.stderr).toOutput(
|
||||||
|
'Downloading "development" Environment Variables for Project vercel-env-pull'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Created other.env file');
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
|
||||||
const rawDevEnv = await fs.readFile(path.join(cwd, 'other.env'));
|
const rawDevEnv = await fs.readFile(path.join(cwd, 'other.env'));
|
||||||
|
|
||||||
@@ -61,8 +69,12 @@ describe('env', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
|
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
|
||||||
const exitCode = await env(client);
|
const exitCodePromise = env(client);
|
||||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
await expect(client.stderr).toOutput(
|
||||||
|
'Downloading "development" Environment Variables for Project vercel-env-pull'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Created other.env file');
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
|
||||||
const rawDevEnv = await fs.readFile(path.join(cwd, 'other.env'));
|
const rawDevEnv = await fs.readFile(path.join(cwd, 'other.env'));
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ describe('inspect', () => {
|
|||||||
client.setArgv('inspect', deployment.url);
|
client.setArgv('inspect', deployment.url);
|
||||||
const exitCode = await inspect(client);
|
const exitCode = await inspect(client);
|
||||||
expect(exitCode).toEqual(0);
|
expect(exitCode).toEqual(0);
|
||||||
expect(
|
await expect(client.stderr).toOutput(
|
||||||
client.mockOutput.mock.calls[0][0].startsWith(
|
`> Fetched deployment "${deployment.url}" in ${user.username}`
|
||||||
`> Fetched deployment "${deployment.url}" in ${user.username}`
|
);
|
||||||
)
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should print error when deployment not found', async () => {
|
it('should print error when deployment not found', async () => {
|
||||||
@@ -23,7 +21,7 @@ describe('inspect', () => {
|
|||||||
client.setArgv('inspect', 'bad.com');
|
client.setArgv('inspect', 'bad.com');
|
||||||
const exitCode = await inspect(client);
|
const exitCode = await inspect(client);
|
||||||
expect(exitCode).toEqual(1);
|
expect(exitCode).toEqual(1);
|
||||||
expect(client.outputBuffer).toEqual(
|
await expect(client.stderr).toOutput(
|
||||||
`Error! Failed to find deployment "bad.com" in ${user.username}\n`
|
`Error! Failed to find deployment "bad.com" in ${user.username}\n`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,22 +5,45 @@ import { useUser } from '../../mocks/user';
|
|||||||
describe('login', () => {
|
describe('login', () => {
|
||||||
it('should not allow the `--token` flag', async () => {
|
it('should not allow the `--token` flag', async () => {
|
||||||
client.setArgv('login', '--token', 'foo');
|
client.setArgv('login', '--token', 'foo');
|
||||||
const exitCode = await login(client);
|
const exitCodePromise = login(client);
|
||||||
expect(exitCode).toEqual(2);
|
await expect(client.stderr).toOutput(
|
||||||
expect(client.outputBuffer).toEqual(
|
|
||||||
'Error! `--token` may not be used with the "login" command\n'
|
'Error! `--token` may not be used with the "login" command\n'
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow login via email as argument', async () => {
|
it('should allow login via email as argument', async () => {
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
client.setArgv('login', user.email);
|
client.setArgv('login', user.email);
|
||||||
const exitCode = await login(client);
|
const exitCodePromise = login(client);
|
||||||
expect(exitCode).toEqual(0);
|
await expect(client.stderr).toOutput(
|
||||||
expect(
|
`Success! Email authentication complete for ${user.email}`
|
||||||
client.outputBuffer.includes(
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interactive', () => {
|
||||||
|
it('should allow login via email', async () => {
|
||||||
|
const user = useUser();
|
||||||
|
client.setArgv('login');
|
||||||
|
const exitCodePromise = login(client);
|
||||||
|
await expect(client.stderr).toOutput(`> Log in to Vercel`);
|
||||||
|
|
||||||
|
// Move down to "Email" option
|
||||||
|
client.stdin.write('\x1B[B'); // Down arrow
|
||||||
|
client.stdin.write('\x1B[B'); // Down arrow
|
||||||
|
client.stdin.write('\x1B[B'); // Down arrow
|
||||||
|
client.stdin.write('\r'); // Return key
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('> Enter your email address:');
|
||||||
|
|
||||||
|
client.stdin.write(`${user.email}\n`);
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
`Success! Email authentication complete for ${user.email}`
|
`Success! Email authentication complete for ${user.email}`
|
||||||
)
|
);
|
||||||
).toEqual(true);
|
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,8 +18,17 @@ describe('pull', () => {
|
|||||||
name: 'vercel-pull-next',
|
name: 'vercel-pull-next',
|
||||||
});
|
});
|
||||||
client.setArgv('pull', cwd);
|
client.setArgv('pull', cwd);
|
||||||
const exitCode = await pull(client);
|
const exitCodePromise = pull(client);
|
||||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
await expect(client.stderr).toOutput(
|
||||||
|
'Downloading "development" Environment Variables for Project vercel-pull-next'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Created .vercel${path.sep}.env.development.local file`
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Downloaded project settings to .vercel${path.sep}project.json`
|
||||||
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
|
||||||
const rawDevEnv = await fs.readFile(
|
const rawDevEnv = await fs.readFile(
|
||||||
path.join(cwd, '.vercel', '.env.development.local')
|
path.join(cwd, '.vercel', '.env.development.local')
|
||||||
@@ -29,23 +38,18 @@ describe('pull', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fail with message to pull without a link and without --env', async () => {
|
it('should fail with message to pull without a link and without --env', async () => {
|
||||||
try {
|
client.stdin.isTTY = false;
|
||||||
process.stdout.isTTY = undefined;
|
|
||||||
|
|
||||||
const cwd = setupFixture('vercel-pull-unlinked');
|
const cwd = setupFixture('vercel-pull-unlinked');
|
||||||
useUser();
|
useUser();
|
||||||
useTeams('team_dummy');
|
useTeams('team_dummy');
|
||||||
|
|
||||||
client.setArgv('pull', cwd);
|
client.setArgv('pull', cwd);
|
||||||
const exitCode = await pull(client);
|
const exitCodePromise = pull(client);
|
||||||
expect(exitCode, client.outputBuffer).toEqual(1);
|
await expect(client.stderr).toOutput(
|
||||||
|
'Command `vercel pull` requires confirmation. Use option "--yes" to confirm.'
|
||||||
expect(client.outputBuffer).toMatch(
|
);
|
||||||
/Command `vercel pull` requires confirmation. Use option "--yes" to confirm./gm
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
process.stdout.isTTY = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail without message to pull without a link and with --env', async () => {
|
it('should fail without message to pull without a link and with --env', async () => {
|
||||||
@@ -54,12 +58,11 @@ describe('pull', () => {
|
|||||||
useTeams('team_dummy');
|
useTeams('team_dummy');
|
||||||
|
|
||||||
client.setArgv('pull', cwd, '--yes');
|
client.setArgv('pull', cwd, '--yes');
|
||||||
const exitCode = await pull(client);
|
const exitCodePromise = pull(client);
|
||||||
expect(exitCode, client.outputBuffer).toEqual(1);
|
await expect(client.stderr).not.toOutput(
|
||||||
|
'Command `vercel pull` requires confirmation. Use option "--yes" to confirm.'
|
||||||
expect(client.outputBuffer).not.toMatch(
|
|
||||||
/Command `vercel pull` requires confirmation. Use option "--yes" to confirm./gm
|
|
||||||
);
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle pulling with env vars (headless mode)', async () => {
|
it('should handle pulling with env vars (headless mode)', async () => {
|
||||||
@@ -81,8 +84,17 @@ describe('pull', () => {
|
|||||||
name: 'vercel-pull-next',
|
name: 'vercel-pull-next',
|
||||||
});
|
});
|
||||||
client.setArgv('pull', cwd);
|
client.setArgv('pull', cwd);
|
||||||
const exitCode = await pull(client);
|
const exitCodePromise = pull(client);
|
||||||
expect(exitCode, client.outputBuffer).toEqual(0);
|
await expect(client.stderr).toOutput(
|
||||||
|
'Downloading "development" Environment Variables for Project vercel-pull-next'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Created .vercel${path.sep}.env.development.local file`
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Downloaded project settings to .vercel${path.sep}project.json`
|
||||||
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
|
||||||
const config = await fs.readJSON(path.join(cwd, '.vercel/project.json'));
|
const config = await fs.readJSON(path.join(cwd, '.vercel/project.json'));
|
||||||
expect(config).toMatchInlineSnapshot(`
|
expect(config).toMatchInlineSnapshot(`
|
||||||
@@ -108,8 +120,17 @@ describe('pull', () => {
|
|||||||
name: 'vercel-pull-next',
|
name: 'vercel-pull-next',
|
||||||
});
|
});
|
||||||
client.setArgv('pull', '--environment=preview', cwd);
|
client.setArgv('pull', '--environment=preview', cwd);
|
||||||
const exitCode = await pull(client);
|
const exitCodePromise = pull(client);
|
||||||
expect(exitCode).toEqual(0);
|
await expect(client.stderr).toOutput(
|
||||||
|
'Downloading "preview" Environment Variables for Project vercel-pull-next'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Created .vercel${path.sep}.env.preview.local file`
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Downloaded project settings to .vercel${path.sep}project.json`
|
||||||
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
|
||||||
const rawPreviewEnv = await fs.readFile(
|
const rawPreviewEnv = await fs.readFile(
|
||||||
path.join(cwd, '.vercel', '.env.preview.local')
|
path.join(cwd, '.vercel', '.env.preview.local')
|
||||||
@@ -130,8 +151,17 @@ describe('pull', () => {
|
|||||||
name: 'vercel-pull-next',
|
name: 'vercel-pull-next',
|
||||||
});
|
});
|
||||||
client.setArgv('pull', '--environment=production', cwd);
|
client.setArgv('pull', '--environment=production', cwd);
|
||||||
const exitCode = await pull(client);
|
const exitCodePromise = pull(client);
|
||||||
expect(exitCode).toEqual(0);
|
await expect(client.stderr).toOutput(
|
||||||
|
'Downloading "production" Environment Variables for Project vercel-pull-next'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Created .vercel${path.sep}.env.production.local file`
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Downloaded project settings to .vercel${path.sep}project.json`
|
||||||
|
);
|
||||||
|
await expect(exitCodePromise).resolves.toEqual(0);
|
||||||
|
|
||||||
const rawProdEnv = await fs.readFile(
|
const rawProdEnv = await fs.readFile(
|
||||||
path.join(cwd, '.vercel', '.env.production.local')
|
path.join(cwd, '.vercel', '.env.production.local')
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ describe('whoami', () => {
|
|||||||
const user = useUser();
|
const user = useUser();
|
||||||
const exitCode = await whoami(client);
|
const exitCode = await whoami(client);
|
||||||
expect(exitCode).toEqual(0);
|
expect(exitCode).toEqual(0);
|
||||||
expect(client.outputBuffer).toEqual(`> ${user.username}\n`);
|
await expect(client.stderr).toOutput(`> ${user.username}\n`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should print only the Vercel username when output is not a TTY', async () => {
|
it('should print only the Vercel username when output is not a TTY', async () => {
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
client.output.isTTY = false;
|
client.stdout.isTTY = false;
|
||||||
const exitCode = await whoami(client);
|
const exitCode = await whoami(client);
|
||||||
expect(exitCode).toEqual(0);
|
expect(exitCode).toEqual(0);
|
||||||
expect(client.outputBuffer).toEqual(`${user.username}\n`);
|
await expect(client.stdout).toOutput(`${user.username}\n`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
34
packages/cli/test/unit/util/confirm.test.ts
Normal file
34
packages/cli/test/unit/util/confirm.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import confirm from '../../../src/util/input/confirm';
|
||||||
|
import { client } from '../../mocks/client';
|
||||||
|
|
||||||
|
describe('confirm()', () => {
|
||||||
|
it('should work with multiple prompts', async () => {
|
||||||
|
// true (explicit)
|
||||||
|
let confirmedPromise = confirm(client, 'Explictly true?', false);
|
||||||
|
await expect(client.stderr).toOutput('Explictly true? [y/N]');
|
||||||
|
client.stdin.write('yes\n');
|
||||||
|
let confirmed = await confirmedPromise;
|
||||||
|
expect(confirmed).toEqual(true);
|
||||||
|
|
||||||
|
// false (explicit)
|
||||||
|
confirmedPromise = confirm(client, 'Explcitly false?', true);
|
||||||
|
await expect(client.stderr).toOutput('Explcitly false? [Y/n]');
|
||||||
|
client.stdin.write('no\n');
|
||||||
|
confirmed = await confirmedPromise;
|
||||||
|
expect(confirmed).toEqual(false);
|
||||||
|
|
||||||
|
// true (default)
|
||||||
|
confirmedPromise = confirm(client, 'Default true?', true);
|
||||||
|
await expect(client.stderr).toOutput('Default true? [Y/n]');
|
||||||
|
client.stdin.write('\n');
|
||||||
|
confirmed = await confirmedPromise;
|
||||||
|
expect(confirmed).toEqual(true);
|
||||||
|
|
||||||
|
// false (default)
|
||||||
|
confirmedPromise = confirm(client, 'Default false?', false);
|
||||||
|
await expect(client.stderr).toOutput('Default false? [y/N]');
|
||||||
|
client.stdin.write('\n');
|
||||||
|
confirmed = await confirmedPromise;
|
||||||
|
expect(confirmed).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,10 +22,7 @@ describe('getRemoteUrl', () => {
|
|||||||
client.output.debugEnabled = true;
|
client.output.debugEnabled = true;
|
||||||
const data = await getRemoteUrl(join(dir, 'git/config'), client.output);
|
const data = await getRemoteUrl(join(dir, 'git/config'), client.output);
|
||||||
expect(data).toBeNull();
|
expect(data).toBeNull();
|
||||||
expect(
|
await expect(client.stderr).toOutput('Error while parsing repo data');
|
||||||
client.outputBuffer.includes('Error while parsing repo data'),
|
|
||||||
'Debug message was not found'
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import { join, sep } from 'path';
|
import { join, sep } from 'path';
|
||||||
// @ts-ignore - Missing types for "alpha-sort"
|
// @ts-ignore - Missing types for "alpha-sort"
|
||||||
import { asc as alpha } from 'alpha-sort';
|
import { asc as alpha } from 'alpha-sort';
|
||||||
import createOutput from '../../../src/util/output';
|
|
||||||
import { staticFiles as getStaticFiles_ } from '../../../src/util/get-files';
|
import { staticFiles as getStaticFiles_ } from '../../../src/util/get-files';
|
||||||
|
import { client } from '../../mocks/client';
|
||||||
|
|
||||||
const output = createOutput({ debug: false });
|
|
||||||
const prefix = `${join(__dirname, '../../fixtures/unit')}${sep}`;
|
const prefix = `${join(__dirname, '../../fixtures/unit')}${sep}`;
|
||||||
const base = (path: string) => path.replace(prefix, '');
|
const base = (path: string) => path.replace(prefix, '');
|
||||||
const fixture = (name: string) => join(prefix, name);
|
const fixture = (name: string) => join(prefix, name);
|
||||||
|
|
||||||
const getStaticFiles = async (dir: string) => {
|
const getStaticFiles = async (dir: string) => {
|
||||||
const files = await getStaticFiles_(dir, {
|
const files = await getStaticFiles_(dir, client);
|
||||||
output,
|
|
||||||
});
|
|
||||||
return normalizeWindowsPaths(files);
|
return normalizeWindowsPaths(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { Framework, frameworks } from '@vercel/frameworks';
|
import { Framework, frameworks } from '@vercel/frameworks';
|
||||||
import editProjectSettings from '../../../../src/util/input/edit-project-settings';
|
import editProjectSettings from '../../../../src/util/input/edit-project-settings';
|
||||||
import { Output } from '../../../../src/util/output';
|
import { client } from '../../../mocks/client';
|
||||||
|
|
||||||
let output: Output;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
output = new Output();
|
|
||||||
output.print = jest.fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
const otherFramework = frameworks.find(
|
const otherFramework = frameworks.find(
|
||||||
fwk => fwk.name === 'Other'
|
fwk => fwk.name === 'Other'
|
||||||
@@ -20,7 +13,7 @@ describe('editProjectSettings', () => {
|
|||||||
describe('with no settings, "Other" framework, and no overrides provided', () => {
|
describe('with no settings, "Other" framework, and no overrides provided', () => {
|
||||||
test('should default all settings to `null` and print user default framework settings', async () => {
|
test('should default all settings to `null` and print user default framework settings', async () => {
|
||||||
const settings = await editProjectSettings(
|
const settings = await editProjectSettings(
|
||||||
output,
|
client,
|
||||||
null,
|
null,
|
||||||
otherFramework,
|
otherFramework,
|
||||||
true,
|
true,
|
||||||
@@ -34,22 +27,13 @@ describe('editProjectSettings', () => {
|
|||||||
installCommand: null,
|
installCommand: null,
|
||||||
outputDirectory: null,
|
outputDirectory: null,
|
||||||
});
|
});
|
||||||
expect((output.print as jest.Mock).mock.calls.length).toBe(5);
|
await expect(client.stderr).toOutput(
|
||||||
expect((output.print as jest.Mock).mock.calls[0][0]).toMatch(
|
'No framework detected. Default Project Settings:'
|
||||||
/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/
|
|
||||||
);
|
);
|
||||||
|
await expect(client.stderr).toOutput('Build Command');
|
||||||
|
await expect(client.stderr).toOutput('Development Command');
|
||||||
|
await expect(client.stderr).toOutput('Install Command');
|
||||||
|
await expect(client.stderr).toOutput('Output Directory');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,29 +47,20 @@ describe('editProjectSettings', () => {
|
|||||||
outputDirectory: 'OUTPUT_DIRECTORY',
|
outputDirectory: 'OUTPUT_DIRECTORY',
|
||||||
};
|
};
|
||||||
const settings = await editProjectSettings(
|
const settings = await editProjectSettings(
|
||||||
output,
|
client,
|
||||||
projectSettings,
|
projectSettings,
|
||||||
otherFramework,
|
otherFramework,
|
||||||
true,
|
true,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
expect(settings).toStrictEqual({ ...projectSettings, framework: null });
|
expect(settings).toStrictEqual({ ...projectSettings, framework: null });
|
||||||
expect((output.print as jest.Mock).mock.calls.length).toBe(5);
|
await expect(client.stderr).toOutput(
|
||||||
expect((output.print as jest.Mock).mock.calls[0][0]).toMatch(
|
'No framework detected. Default Project Settings:'
|
||||||
/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/
|
|
||||||
);
|
);
|
||||||
|
await expect(client.stderr).toOutput('Build Command');
|
||||||
|
await expect(client.stderr).toOutput('Development Command');
|
||||||
|
await expect(client.stderr).toOutput('Install Command');
|
||||||
|
await expect(client.stderr).toOutput('Output Directory');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,32 +74,21 @@ describe('editProjectSettings', () => {
|
|||||||
outputDirectory: 'OUTPUT_DIRECTORY',
|
outputDirectory: 'OUTPUT_DIRECTORY',
|
||||||
};
|
};
|
||||||
const settings = await editProjectSettings(
|
const settings = await editProjectSettings(
|
||||||
output,
|
client,
|
||||||
projectSettings,
|
projectSettings,
|
||||||
nextJSFramework,
|
nextJSFramework,
|
||||||
true,
|
true,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
expect((output.print as jest.Mock).mock.calls.length).toBe(5);
|
|
||||||
expect((output.print as jest.Mock).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(settings).toStrictEqual({
|
expect(settings).toStrictEqual({
|
||||||
...projectSettings,
|
...projectSettings,
|
||||||
framework: nextJSFramework.slug,
|
framework: nextJSFramework.slug,
|
||||||
});
|
});
|
||||||
|
await expect(client.stderr).toOutput('Auto-detected Project Settings');
|
||||||
|
await expect(client.stderr).toOutput('Build Command');
|
||||||
|
await expect(client.stderr).toOutput('Development Command');
|
||||||
|
await expect(client.stderr).toOutput('Install Command');
|
||||||
|
await expect(client.stderr).toOutput('Output Directory');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,42 +110,26 @@ describe('editProjectSettings', () => {
|
|||||||
outputDirectory: 'OUTPUT_DIRECTORY',
|
outputDirectory: 'OUTPUT_DIRECTORY',
|
||||||
};
|
};
|
||||||
const settings = await editProjectSettings(
|
const settings = await editProjectSettings(
|
||||||
output,
|
client,
|
||||||
projectSettings,
|
projectSettings,
|
||||||
nextJSFramework,
|
nextJSFramework,
|
||||||
true,
|
true,
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
expect((output.print as jest.Mock).mock.calls.length).toBe(9);
|
|
||||||
expect((output.print as jest.Mock).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(
|
|
||||||
/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(
|
|
||||||
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
|
|
||||||
);
|
|
||||||
expect((output.print as jest.Mock).mock.calls[8][0]).toMatch(
|
|
||||||
/Auto-detected Project Settings/
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(settings).toStrictEqual(overrides);
|
expect(settings).toStrictEqual(overrides);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Local settings detected in vercel.json:'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Build Command:');
|
||||||
|
await expect(client.stderr).toOutput('Ignore Command:');
|
||||||
|
await expect(client.stderr).toOutput('Development Command:');
|
||||||
|
await expect(client.stderr).toOutput('Framework:');
|
||||||
|
await expect(client.stderr).toOutput('Install Command:');
|
||||||
|
await expect(client.stderr).toOutput('Output Directory:');
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Merging default Project Settings for Svelte. Previously listed overrides are prioritized.'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Auto-detected Project Settings');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,41 +144,26 @@ describe('editProjectSettings', () => {
|
|||||||
outputDirectory: 'OUTPUT_DIRECTORY',
|
outputDirectory: 'OUTPUT_DIRECTORY',
|
||||||
};
|
};
|
||||||
const settings = await editProjectSettings(
|
const settings = await editProjectSettings(
|
||||||
output,
|
client,
|
||||||
null,
|
null,
|
||||||
nextJSFramework,
|
nextJSFramework,
|
||||||
true,
|
true,
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
expect((output.print as jest.Mock).mock.calls.length).toBe(9);
|
|
||||||
expect((output.print as jest.Mock).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(
|
|
||||||
/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(
|
|
||||||
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
|
|
||||||
);
|
|
||||||
expect((output.print as jest.Mock).mock.calls[8][0]).toMatch(
|
|
||||||
/Auto-detected Project Settings/
|
|
||||||
);
|
|
||||||
expect(settings).toStrictEqual(overrides);
|
expect(settings).toStrictEqual(overrides);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Local settings detected in vercel.json:'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Build Command:');
|
||||||
|
await expect(client.stderr).toOutput('Ignore Command:');
|
||||||
|
await expect(client.stderr).toOutput('Development Command:');
|
||||||
|
await expect(client.stderr).toOutput('Framework:');
|
||||||
|
await expect(client.stderr).toOutput('Install Command:');
|
||||||
|
await expect(client.stderr).toOutput('Output Directory:');
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Merging default Project Settings for Svelte. Previously listed overrides are prioritized.'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Auto-detected Project Settings');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -245,42 +178,26 @@ describe('editProjectSettings', () => {
|
|||||||
outputDirectory: 'OUTPUT_DIRECTORY',
|
outputDirectory: 'OUTPUT_DIRECTORY',
|
||||||
};
|
};
|
||||||
const settings = await editProjectSettings(
|
const settings = await editProjectSettings(
|
||||||
output,
|
client,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
expect((output.print as jest.Mock).mock.calls.length).toBe(9);
|
|
||||||
expect((output.print as jest.Mock).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(
|
|
||||||
/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(
|
|
||||||
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
|
|
||||||
);
|
|
||||||
expect((output.print as jest.Mock).mock.calls[8][0]).toMatch(
|
|
||||||
/Auto-detected Project Settings/
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(settings).toStrictEqual(overrides);
|
expect(settings).toStrictEqual(overrides);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Local settings detected in vercel.json:'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Build Command:');
|
||||||
|
await expect(client.stderr).toOutput('Ignore Command:');
|
||||||
|
await expect(client.stderr).toOutput('Development Command:');
|
||||||
|
await expect(client.stderr).toOutput('Framework:');
|
||||||
|
await expect(client.stderr).toOutput('Install Command:');
|
||||||
|
await expect(client.stderr).toOutput('Output Directory:');
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Merging default Project Settings for Svelte. Previously listed overrides are prioritized.'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Auto-detected Project Settings');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vercel/client",
|
"name": "@vercel/client",
|
||||||
"version": "12.0.2",
|
"version": "12.0.4",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"typings": "dist/index.d.ts",
|
"typings": "dist/index.d.ts",
|
||||||
"homepage": "https://vercel.com",
|
"homepage": "https://vercel.com",
|
||||||
@@ -42,7 +42,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/build-utils": "4.2.0",
|
"@vercel/build-utils": "5.0.0",
|
||||||
|
"@vercel/routing-utils": "1.13.5",
|
||||||
"@zeit/fetch": "5.2.0",
|
"@zeit/fetch": "5.2.0",
|
||||||
"async-retry": "1.2.3",
|
"async-retry": "1.2.3",
|
||||||
"async-sema": "3.0.0",
|
"async-sema": "3.0.0",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import type {
|
||||||
Builder,
|
Builder,
|
||||||
BuilderFunctions,
|
BuilderFunctions,
|
||||||
ProjectSettings,
|
ProjectSettings,
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
import { Header, Route, Redirect, Rewrite } from '@vercel/routing-utils';
|
import type { Header, Route, Redirect, Rewrite } from '@vercel/routing-utils';
|
||||||
|
|
||||||
export { DeploymentEventType } from './utils';
|
export { DeploymentEventType } from './utils';
|
||||||
|
|
||||||
|
|||||||
1
packages/fs-detectors/.gitignore
vendored
Normal file
1
packages/fs-detectors/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/dist
|
||||||
5
packages/fs-detectors/jest.config.js
Normal file
5
packages/fs-detectors/jest.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
};
|
||||||
40
packages/fs-detectors/package.json
Normal file
40
packages/fs-detectors/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@vercel/fs-detectors",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Vercel filesystem detectors",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/vercel/vercel.git",
|
||||||
|
"directory": "packages/fs-detectors"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "tsc",
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "yarn jest --env node --verbose --runInBand --bail test/unit.*test.*",
|
||||||
|
"test-unit": "yarn test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vercel/frameworks": "1.0.2",
|
||||||
|
"@vercel/routing-utils": "1.13.5",
|
||||||
|
"glob": "8.0.3",
|
||||||
|
"js-yaml": "4.1.0",
|
||||||
|
"minimatch": "3.0.4",
|
||||||
|
"semver": "6.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/glob": "7.2.0",
|
||||||
|
"@types/jest": "27.5.1",
|
||||||
|
"@types/js-yaml": "4.0.5",
|
||||||
|
"@types/minimatch": "3.0.5",
|
||||||
|
"@types/node": "12.12.20",
|
||||||
|
"@types/semver": "7.3.10",
|
||||||
|
"@vercel/build-utils": "4.2.0",
|
||||||
|
"typescript": "4.3.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import minimatch from 'minimatch';
|
import minimatch from 'minimatch';
|
||||||
import { valid as validSemver } from 'semver';
|
import { valid as validSemver } from 'semver';
|
||||||
import { parse as parsePath, extname } from 'path';
|
import { parse as parsePath, extname } from 'path';
|
||||||
import { Route, Source } from '@vercel/routing-utils';
|
import type { Route, Source } from '@vercel/routing-utils';
|
||||||
import frameworkList, { Framework } from '@vercel/frameworks';
|
import frameworkList, { Framework } from '@vercel/frameworks';
|
||||||
import {
|
import type {
|
||||||
PackageJson,
|
PackageJson,
|
||||||
Builder,
|
Builder,
|
||||||
Config,
|
Config,
|
||||||
BuilderFunctions,
|
BuilderFunctions,
|
||||||
ProjectSettings,
|
ProjectSettings,
|
||||||
} from './types';
|
} from '@vercel/build-utils';
|
||||||
import { isOfficialRuntime } from './';
|
import { isOfficialRuntime } from './is-official-runtime';
|
||||||
const slugToFramework = new Map<string | null, Framework>(
|
const slugToFramework = new Map<string | null, Framework>(
|
||||||
frameworkList.map(f => [f.slug, f])
|
frameworkList.map(f => [f.slug, f])
|
||||||
);
|
);
|
||||||
@@ -5,7 +5,7 @@ import type {
|
|||||||
BuilderFunctions,
|
BuilderFunctions,
|
||||||
PackageJson,
|
PackageJson,
|
||||||
ProjectSettings,
|
ProjectSettings,
|
||||||
} from './types';
|
} from '@vercel/build-utils';
|
||||||
|
|
||||||
interface Metadata {
|
interface Metadata {
|
||||||
plugins: string[];
|
plugins: string[];
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Framework, FrameworkDetectionItem } from '@vercel/frameworks';
|
import type { Framework, FrameworkDetectionItem } from '@vercel/frameworks';
|
||||||
import { DetectorFilesystem } from './detectors/filesystem';
|
import { DetectorFilesystem } from './detectors/filesystem';
|
||||||
|
|
||||||
interface BaseFramework {
|
interface BaseFramework {
|
||||||
23
packages/fs-detectors/src/index.ts
Normal file
23
packages/fs-detectors/src/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export {
|
||||||
|
detectBuilders,
|
||||||
|
detectOutputDirectory,
|
||||||
|
detectApiDirectory,
|
||||||
|
detectApiExtensions,
|
||||||
|
} from './detect-builders';
|
||||||
|
export { detectFileSystemAPI } from './detect-file-system-api';
|
||||||
|
export { detectFramework } from './detect-framework';
|
||||||
|
export { getProjectPaths } from './get-project-paths';
|
||||||
|
export { DetectorFilesystem } from './detectors/filesystem';
|
||||||
|
export { workspaceManagers } from './workspaces/workspace-managers';
|
||||||
|
export {
|
||||||
|
getWorkspaces,
|
||||||
|
GetWorkspaceOptions,
|
||||||
|
Workspace,
|
||||||
|
WorkspaceType,
|
||||||
|
} from './workspaces/get-workspaces';
|
||||||
|
export {
|
||||||
|
getWorkspacePackagePaths,
|
||||||
|
GetWorkspacePackagePathsOptions,
|
||||||
|
} from './workspaces/get-workspace-package-paths';
|
||||||
|
export { monorepoManagers } from './monorepos/monorepo-managers';
|
||||||
|
export { isOfficialRuntime, isStaticRuntime } from './is-official-runtime';
|
||||||
21
packages/fs-detectors/src/is-official-runtime.ts
Normal file
21
packages/fs-detectors/src/is-official-runtime.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Helper function to support both `@vercel` and legacy `@now` official Runtimes.
|
||||||
|
*/
|
||||||
|
export const isOfficialRuntime = (desired: string, name?: string): boolean => {
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
name === `@vercel/${desired}` ||
|
||||||
|
name === `@now/${desired}` ||
|
||||||
|
name.startsWith(`@vercel/${desired}@`) ||
|
||||||
|
name.startsWith(`@now/${desired}@`)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Helper function to detect both `@vercel/static` and legacy `@now/static` official Runtimes.
|
||||||
|
*/
|
||||||
|
export const isStaticRuntime = (name?: string): boolean => {
|
||||||
|
return isOfficialRuntime('static', name);
|
||||||
|
};
|
||||||
@@ -6,8 +6,8 @@ import type { Framework } from '@vercel/frameworks';
|
|||||||
* This list is designed to work with the @see {@link detectFramework} function.
|
* This list is designed to work with the @see {@link detectFramework} function.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* import { monorepoManagers as frameworkList } from '@vercel/build-utils'
|
* import { monorepoManagers as frameworkList } from '@vercel/fs-detectors'
|
||||||
* import { detectFramework } from '@vercel/build-utils'
|
* import { detectFramework } from '@vercel/fs-detectors'
|
||||||
*
|
*
|
||||||
* const fs = new GitDetectorFilesystem(...)
|
* const fs = new GitDetectorFilesystem(...)
|
||||||
* detectFramwork({ fs, frameworkList }) // returns the 'slug' field if detected, otherwise null
|
* detectFramwork({ fs, frameworkList }) // returns the 'slug' field if detected, otherwise null
|
||||||
@@ -3,7 +3,7 @@ import { DetectorFilesystem } from '../detectors/filesystem';
|
|||||||
|
|
||||||
type GlobFs = typeof fs;
|
type GlobFs = typeof fs;
|
||||||
|
|
||||||
function normalizePath(path: string) {
|
function removeWindowsPrefix(path: string) {
|
||||||
// on windows, this will return a path like
|
// on windows, this will return a path like
|
||||||
// D:/c/package.json
|
// D:/c/package.json
|
||||||
// since we abstract the filesystem, we need to remove windows specific info from the path
|
// since we abstract the filesystem, we need to remove windows specific info from the path
|
||||||
@@ -18,7 +18,7 @@ export function getGlobFs(_fs: DetectorFilesystem): GlobFs {
|
|||||||
callback: (err: NodeJS.ErrnoException | null, files: string[]) => void
|
callback: (err: NodeJS.ErrnoException | null, files: string[]) => void
|
||||||
): void => {
|
): void => {
|
||||||
_fs
|
_fs
|
||||||
.readdir(normalizePath(String(path)))
|
.readdir(removeWindowsPrefix(String(path)))
|
||||||
.then(stats =>
|
.then(stats =>
|
||||||
callback(
|
callback(
|
||||||
null,
|
null,
|
||||||
@@ -36,7 +36,7 @@ export function getGlobFs(_fs: DetectorFilesystem): GlobFs {
|
|||||||
) => void
|
) => void
|
||||||
): void => {
|
): void => {
|
||||||
_fs
|
_fs
|
||||||
.isFile(normalizePath(String(path)))
|
.isFile(removeWindowsPrefix(String(path)))
|
||||||
.then(isPathAFile => {
|
.then(isPathAFile => {
|
||||||
callback(null, {
|
callback(null, {
|
||||||
ino: 0,
|
ino: 0,
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user