Compare commits

...

10 Commits

Author SHA1 Message Date
Steven
621b53bc49 Publish Stable
- @vercel/build-utils@5.0.0
 - vercel@26.0.0
 - @vercel/client@12.0.4
 - @vercel/fs-detectors@1.0.0
 - @vercel/go@2.0.4
 - @vercel/next@3.1.3
 - @vercel/node@2.4.0
 - @vercel/python@3.0.4
 - @vercel/redwood@1.0.5
 - @vercel/remix@1.0.5
 - @vercel/ruby@1.3.12
 - @vercel/static-build@1.0.4
2022-07-05 08:47:49 -04:00
JJ Kasper
728b620355 Update trailing slash handling for _next/data resolving (#8080) 2022-07-04 09:52:08 -05:00
Luc Leray
7d16395038 [build-utils] Stricter getNodeBinPath return type (#8082)
In https://github.com/vercel/vercel/pull/8058, I made `getNodeBinPath` return type too broad. The function can never return `undefined` so we can make it more strict.
2022-07-04 13:22:35 +00:00
Lee Robinson
59e1259688 [examples] Remove stray console.log. (#8070) 2022-07-01 22:47:36 +00:00
Nathan Rajlich
169242157e [cli] Fix Middleware "matchers" with query string in vc dev (#8069)
`matchers` config in Middleware was not being properly… matched… in `vc dev` when a query string was present.
2022-07-01 20:38:02 +00:00
Steven
db10ffd679 [build-utils][next][redwood][remix][static-build] Fix corepack path prepend (#8065)
This PR fixes a bug where corepack (#7871) was not correctly setup because the lockfile autodetection and node version autodetection was overriding the PATH.

It also fixes a bug where the log output was printed twice because we incorrectly prepended the PATH twice.
2022-07-01 19:33:21 +00:00
Steven
c0d0744c4e [tests] Add yarn.lock to saber test fixture (#8068)
The Saber example had a lock file but the test did not.

This PR copies the lock file since the latest version of Saber doesn't work with the latest Vue.

```
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './package' is not defined by "exports" in /vercel/path0/node_modules/vue/package.json
  at new NodeError (node:internal/errors:372:5)
  at throwExportsNotFound (node:internal/modules/esm/resolve:472:9)
  at packageExportsResolve (node:internal/modules/esm/resolve:753:3)
  at resolveExports (node:internal/modules/cjs/loader:482:36)
  at Function.Module._findPath (node:internal/modules/cjs/loader:522:31)
  at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)
  at Function.resolve (node:internal/modules/cjs/helpers:108:19)
  at module.exports (/vercel/path0/node_modules/saber/dist/webpack/webpack.config.js:30:58)
  at Saber.getWebpackConfig (/vercel/path0/node_modules/saber/dist/index.js:195:58)
  at VueRenderer.build (/vercel/path0/node_modules/saber/dist/vue-renderer/index.js:232:39)
```
2022-07-01 19:04:44 +00:00
Steven
9da67423a5 [node] Fix public TypeScript types (#8064)
There was a regression some time between [1.12.2-canary.6](https://unpkg.com/browse/@vercel/node@1.12.2-canary.6/dist/index.d.ts) and [1.12.2-canary.7](https://unpkg.com/browse/@vercel/node@1.12.2-canary.7/dist/index.d.ts) where the `index.d.ts` types changed.

This PR reverts back to the old behavior so users can properly use

```ts
import { VercelRequest, VercelResponse } from '@vercel/node';
```

The existing test was also changed to use the result of the build rather than copy at the beginning

cfae7ec3c2/packages/node/test/fixtures/15-helpers/ts/index.ts (L1)

- Fixes https://github.com/vercel/vercel/issues/7951
2022-06-30 22:45:49 +00:00
Steven
51fe09d5e9 [build-utils][fs-detectors][cli] MAJOR: move some of build-utils into new fs-detectors (#8054)
The `@vercel/build-utils` package was meant be shared functions necessary for writing a Vercel Builder (aka Runtime).

This package has since bloated into the catch-all package for anything that wasn't a Builder.

This PR removes the bloat in favor of a new package, `@vercel/fs-detectors`. It also removes the need for `@vercel/build-utils` to have a dependency on `@vercel/frameworks`.

- Related to #7951
2022-06-30 21:14:07 +00:00
Nathan Rajlich
695bfbdd60 [cli] Add full stdio mockability for unit tests (#8052)
This PR is a follow-up to #8039, which provides an intuitive syntax for writing unit tests for interactive CLI commands.

The heart of this is the new `await expect(stream).toOutput(test)` custom Jest matcher, which is intended for use with the mock Client `stdout` and `stderr` stream properties. The `test` is a string that will wait for the stream to output via "data" events until a match is found, or it will timeout (after 3 seconds by default). The timeout error has nice Jest-style formatting so that you can easily identify what was output:

<img width="553" alt="Screen Shot 2022-06-29 at 10 33 06 PM" src="https://user-images.githubusercontent.com/71256/176600324-cb1ebecb-e891-42d9-bdc9-4864d3594a8c.png">

Below is an example of a unit test that was added for an interactive `vc login` session:

```typescript
it('should allow login via email', async () => {
  const user = useUser();

  const exitCodePromise = login(client);

  // Wait for login interactive prompt
  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

  // Wait for email input prompt
  await expect(client.stderr).toOutput('> Enter your email address:');

  // Write user email address into prompt
  client.stdin.write(`${user.email}\n`);

  // Wait for login success message
  await expect(client.stderr).toOutput(
    `Success! Email authentication complete for ${user.email}`
  );

  // Assert that the `login()` command returned 0 exit code
  await expect(exitCodePromise).resolves.toEqual(0);
});
```

**Note:**  as a consequence of this PR, prompts are now written to stderr instead of stdout, so this change _may_ be considered a breaking change, in which case we should tag it for major release.
2022-06-30 20:17:22 +00:00
215 changed files with 16594 additions and 757 deletions

View File

@@ -24,7 +24,6 @@ export function sendToVercelAnalytics(metric) {
speed: getConnectionSpeed(),
};
console.log({ body });
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded',

View File

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

View File

@@ -177,7 +177,7 @@ export async function getNodeBinPath({
cwd,
}: {
cwd: string;
}): Promise<string | undefined> {
}): Promise<string> {
const { code, stdout, stderr } = await execAsync('npm', ['bin'], {
cwd,
prettyCommand: 'npm bin',
@@ -233,10 +233,23 @@ export function getSpawnOptions(
};
if (!meta.isDev) {
// Ensure that the selected Node version is at the beginning of the `$PATH`
opts.env.PATH = `/node${nodeVersion.major}/bin${path.delimiter}${
opts.env.PATH || process.env.PATH
}`;
let found = false;
const oldPath = 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;
@@ -474,20 +487,31 @@ export function getEnvForPackageManager({
env: { [x: string]: string | undefined };
}) {
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 (
typeof lockfileVersion === 'number' &&
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`
newEnv.PATH = `/node16/bin-npm7${path.delimiter}${env.PATH}`;
console.log('Detected `package-lock.json` generated by npm 7...');
newEnv.PATH = `${npm7}${path.delimiter}${oldPath}`;
console.log('Detected `package-lock.json` generated by npm 7+...');
}
} 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`
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...');
}
} else {

View File

@@ -80,16 +80,6 @@ export {
};
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 { normalizePath } from './fs/normalize-path';
@@ -97,35 +87,3 @@ export * from './should-serve';
export * from './schemas';
export * from './types';
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';

View File

@@ -1,10 +0,0 @@
{
"functions": {
"api/users.rb": {
"memory": 3008
},
"api/doesnt-exist.rb": {
"memory": 768
}
}
}

View File

@@ -1 +0,0 @@
# project/[aid]/[bid]/index.py

View File

@@ -1 +0,0 @@
This file should also be included

View File

@@ -1,7 +0,0 @@
{
"functions": {
"api/users/post.py": {
"memory": 3008
}
}
}

View File

@@ -5,7 +5,6 @@ import {
testDeployment,
// @ts-ignore
} from '../../../test/lib/deployment/test-deployment';
import { glob, detectBuilders } from '../src';
jest.setTimeout(4 * 60 * 1000);
@@ -32,11 +31,6 @@ const skipFixtures: string[] = [
'08-zero-config-middleman',
'21-npm-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
@@ -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();
});

View File

@@ -38,6 +38,38 @@ describe('Test `getEnvForPackageManager()`', () => {
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',
args: {
@@ -101,6 +133,38 @@ describe('Test `getEnvForPackageManager()`', () => {
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',
args: {

View 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);
});
}
});

View File

@@ -18,6 +18,8 @@ import {
Meta,
} from '../src';
jest.setTimeout(7 * 1000);
async function expectBuilderError(promise: Promise<any>, pattern: string) {
let result;
try {

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "25.2.3",
"version": "26.0.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -42,15 +42,15 @@
"node": ">= 14"
},
"dependencies": {
"@vercel/build-utils": "4.2.1",
"@vercel/go": "2.0.3",
"@vercel/next": "3.1.2",
"@vercel/node": "2.3.3",
"@vercel/python": "3.0.3",
"@vercel/redwood": "1.0.4",
"@vercel/remix": "1.0.4",
"@vercel/ruby": "1.3.11",
"@vercel/static-build": "1.0.3",
"@vercel/build-utils": "5.0.0",
"@vercel/go": "2.0.4",
"@vercel/next": "3.1.3",
"@vercel/node": "2.4.0",
"@vercel/python": "3.0.4",
"@vercel/redwood": "1.0.5",
"@vercel/remix": "1.0.5",
"@vercel/ruby": "1.3.12",
"@vercel/static-build": "1.0.4",
"update-notifier": "5.1.0"
},
"devDependencies": {
@@ -95,8 +95,9 @@
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.0.3",
"@vercel/client": "12.0.4",
"@vercel/frameworks": "1.0.2",
"@vercel/fs-detectors": "1.0.0",
"@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2",

View File

@@ -127,7 +127,7 @@ export default async function ({ creditCards, clear = false, contextName }) {
}
console.log(''); // New line
const stopSpinner = wait('Saving card');
const stopSpinner = wait(process.stderr, 'Saving card');
try {
const res = await creditCards.add({

View File

@@ -174,7 +174,7 @@ export default async client => {
)} ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(cards);
cardId = await listInput({
cardId = await listInput(client, {
message,
choices,
separator: true,
@@ -251,7 +251,7 @@ export default async client => {
)} under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`;
const choices = buildInquirerChoices(cards);
cardId = await listInput({
cardId = await listInput(client, {
message,
choices,
separator: true,

View File

@@ -14,7 +14,6 @@ import logo from '../../util/output/logo';
import getArgs from '../../util/get-args';
import Client from '../../util/client';
import { getPkgName } from '../../util/pkg-name';
import { Output } from '../../util/output';
import { Deployment, PaginationOptions } from '../../types';
import { normalizeURL } from '../../util/bisect/normalize-url';
@@ -86,10 +85,10 @@ export default async function main(client: Client): Promise<number> {
let 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 =
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 run = argv['--run'] || '';
const openEnabled = argv['--open'] || false;
@@ -143,7 +142,7 @@ export default async function main(client: Client): Promise<number> {
if (!subpath) {
subpath = await prompt(
output,
client,
`Specify the URL subpath where the bug occurs:`
);
}
@@ -391,10 +390,10 @@ function getCommit(deployment: DeploymentV6) {
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
while (true) {
const { val } = await inquirer.prompt({
const { val } = await client.prompt({
type: 'input',
name: 'val',
message,
@@ -402,7 +401,7 @@ async function prompt(output: Output, message: string): Promise<string> {
if (val) {
return val;
} else {
output.error('A value must be specified');
client.output.error('A value must be specified');
}
}
}

View File

@@ -3,7 +3,6 @@ import chalk from 'chalk';
import dotenv from 'dotenv';
import { join, normalize, relative, resolve } from 'path';
import {
detectBuilders,
normalizePath,
Files,
FileFsRef,
@@ -17,6 +16,7 @@ import {
BuildResultV3,
NowBuildError,
} from '@vercel/build-utils';
import { detectBuilders } from '@vercel/fs-detectors';
import minimatch from 'minimatch';
import {
appendRoutesToPhase,

View File

@@ -160,9 +160,9 @@ 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
const pathValidation = await validatePaths(client, paths);

View File

@@ -46,7 +46,11 @@ export default async function init(
const exampleList = examples.filter(x => x.visible).map(x => x.name);
if (!name) {
const chosen = await chooseFromDropdown('Select example:', exampleList);
const chosen = await chooseFromDropdown(
client,
'Select example:',
exampleList
);
if (!chosen) {
output.log('Aborted');
@@ -90,14 +94,18 @@ async function fetchExampleList(client: Client) {
/**
* 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 => ({
name,
value: name,
short: name,
}));
return listInput({
return listInput(client, {
message,
choices,
});

View File

@@ -83,7 +83,7 @@ export default async function main(client: Client, desiredSlug?: string) {
];
output.stopSpinner();
desiredSlug = await listInput({
desiredSlug = await listInput(client, {
message: 'Switch to:',
choices,
eraseFinalAnswer: true,

View File

@@ -54,12 +54,12 @@ export default async (client: Client): Promise<number> => {
throw err;
}
if (output.isTTY) {
if (client.stdout.isTTY) {
output.log(contextName);
} else {
// If stdout is not a TTY, then only print the username
// to support piping the output to another file / exe
output.print(`${contextName}\n`, { w: process.stdout });
client.stdout.write(`${contextName}\n`);
}
return 0;

View File

@@ -23,7 +23,7 @@ import * as Sentry from '@sentry/node';
import hp from './util/humanize-path';
import commands from './commands';
import pkg from './util/pkg';
import createOutput from './util/output';
import { Output } from './util/output';
import cmd from './util/output/cmd';
import info from './util/output/info';
import error from './util/output/error';
@@ -109,7 +109,7 @@ const main = async () => {
}
const isDebugging = argv['--debug'];
const output = createOutput({ debug: isDebugging });
const output = new Output(process.stderr, { debug: isDebugging });
debug = output.debug;
@@ -389,6 +389,7 @@ const main = async () => {
apiUrl,
stdin: process.stdin,
stdout: process.stdout,
stderr: output.stream,
output,
config,
authConfig,
@@ -798,7 +799,5 @@ process.on('uncaughtException', handleUnexpected);
main()
.then(exitCode => {
process.exitCode = exitCode;
// @ts-ignore - "nowExit" is a non-standard event name
process.emit('nowExit');
})
.catch(handleUnexpected);

View File

@@ -1,3 +1,5 @@
import type { Readable, Writable } from 'stream';
export type ProjectSettings = import('@vercel/build-utils').ProjectSettings;
export type Primitive =
@@ -442,3 +444,19 @@ export interface BuildOutput {
layers?: string[];
} | 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;
}

View File

@@ -1,3 +1,5 @@
import { bold } from 'chalk';
import inquirer from 'inquirer';
import { EventEmitter } from 'events';
import { URLSearchParams } from 'url';
import { parse as parseUrl } from 'url';
@@ -11,10 +13,16 @@ import printIndications from './print-indications';
import reauthenticate from './login/reauthenticate';
import { SAMLError } from './login/types';
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 { APIError } from './errors-ts';
import { bold } from 'chalk';
const isSAMLError = (v: any): v is SAMLError => {
return v && v.saml;
@@ -28,12 +36,10 @@ export interface FetchOptions extends Omit<RequestInit, 'body'> {
accountId?: string;
}
export interface ClientOptions {
export interface ClientOptions extends Stdio {
argv: string[];
apiUrl: string;
authConfig: AuthConfig;
stdin: NodeJS.ReadStream;
stdout: NodeJS.WriteStream;
output: Output;
config: GlobalConfig;
localConfig?: VercelConfig;
@@ -43,15 +49,17 @@ export const isJSONObject = (v: any): v is JSONObject => {
return v && typeof v == 'object' && v.constructor === Object;
};
export default class Client extends EventEmitter {
export default class Client extends EventEmitter implements Stdio {
argv: string[];
apiUrl: string;
authConfig: AuthConfig;
stdin: NodeJS.ReadStream;
stdout: NodeJS.WriteStream;
stdin: ReadableTTY;
stdout: WritableTTY;
stderr: WritableTTY;
output: Output;
config: GlobalConfig;
localConfig?: VercelConfig;
prompt!: inquirer.PromptModule;
private requestIdCounter: number;
constructor(opts: ClientOptions) {
@@ -61,10 +69,12 @@ export default class Client extends EventEmitter {
this.authConfig = opts.authConfig;
this.stdin = opts.stdin;
this.stdout = opts.stdout;
this.stderr = opts.stderr;
this.output = opts.output;
this.config = opts.config;
this.localConfig = opts.localConfig;
this.requestIdCounter = 1;
this._createPromptModule();
}
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
@@ -130,7 +140,7 @@ export default class Client extends EventEmitter {
return this.retry(async bail => {
const res = await this._fetch(url, opts);
printIndications(res);
printIndications(this, res);
if (!res.ok) {
const error = await responseError(res);
@@ -186,4 +196,11 @@ export default class Client extends EventEmitter {
_onRetry = (error: Error) => {
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,
});
}
}

View File

@@ -13,8 +13,8 @@ import {
Lambda,
FileBlob,
FileFsRef,
isOfficialRuntime,
} from '@vercel/build-utils';
import { isOfficialRuntime } from '@vercel/fs-detectors';
import plural from 'pluralize';
import minimatch from 'minimatch';

View File

@@ -36,12 +36,14 @@ import {
StartDevServerResult,
FileFsRef,
PackageJson,
spawnCommand,
} from '@vercel/build-utils';
import {
detectBuilders,
detectApiDirectory,
detectApiExtensions,
spawnCommand,
isOfficialRuntime,
} from '@vercel/build-utils';
} from '@vercel/fs-detectors';
import frameworkList from '@vercel/frameworks';
import cmd from '../output/cmd';

View File

@@ -1,5 +1,5 @@
import { resolve } from 'path';
import fs from 'fs-extra';
import { resolve } from 'path';
import { getVercelIgnore } from '@vercel/client';
import uniqueStrings from './unique-strings';
import { Output } from './output/create-output';

View File

@@ -530,7 +530,7 @@ export default class Now extends EventEmitter {
`${opts.method || 'GET'} ${this._apiUrl}${_url} ${opts.body || ''}`,
fetch(`${this._apiUrl}${_url}`, { ...opts, body })
);
printIndications(res);
printIndications(this._client, res);
return res;
}

View File

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

View File

@@ -1,5 +1,4 @@
import Client from '../client';
import inquirer from 'inquirer';
import confirm from './confirm';
import getProjectByIdOrName from '../projects/get-project-by-id-or-name';
import chalk from 'chalk';
@@ -79,11 +78,7 @@ export default async function inputProject(
let project: Project | ProjectNotFound | null = null;
while (!project || project instanceof ProjectNotFound) {
const prompt = inquirer.createPromptModule({
input: client.stdin,
output: client.stdout,
});
const answers = await prompt({
const answers = await client.prompt({
type: 'input',
name: 'existingProjectName',
message: `Whats the name of your existing project?`,
@@ -114,7 +109,7 @@ export default async function inputProject(
let newProjectName: string | null = null;
while (!newProjectName) {
const answers = await inquirer.prompt({
const answers = await client.prompt({
type: 'input',
name: 'newProjectName',
message: `Whats your projects name?`,

View File

@@ -1,6 +1,5 @@
import path from 'path';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { validateRootDirectory } from '../validate-paths';
import Client from '../client';
@@ -15,11 +14,7 @@ export async function inputRootDirectory(
// eslint-disable-next-line no-constant-condition
while (true) {
const prompt = inquirer.createPromptModule({
input: client.stdin,
output: client.stdout,
});
const { rootDirectory } = await prompt({
const { rootDirectory } = await client.prompt({
type: 'input',
name: 'rootDirectory',
message: `In which directory is your code located?`,

View File

@@ -1,5 +1,6 @@
import inquirer from 'inquirer';
import stripAnsi from 'strip-ansi';
import Client from '../client';
import eraseLines from '../output/erase-lines';
interface ListEntry {
@@ -35,21 +36,24 @@ function getLength(input: string): number {
return biggestLength;
}
export default async function list({
message = 'the question',
// eslint-disable-line no-unused-vars
choices: _choices = [
{
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`,
eraseFinalAnswer = false, // If true, the line with the final answer that inquirer prints will be erased before returning
}: ListOptions): Promise<string> {
export default async function list(
client: Client,
{
message = 'the question',
// eslint-disable-line no-unused-vars
choices: _choices = [
{
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`,
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');
let biggestLength = 0;
@@ -106,7 +110,7 @@ export default async function list({
choices.push(abortSeparator, _abort);
}
const answer = await inquirer.prompt({
const answer = await client.prompt({
name: 'value',
type: 'list',
default: selected,

View File

@@ -1,12 +1,13 @@
import chalk from 'chalk';
import type { ReadableTTY, WritableTTY } from '../../types';
type Options = {
abortSequences?: Set<string>;
defaultValue?: boolean;
noChar?: string;
resolveChars?: Set<string>;
stdin: NodeJS.ReadStream;
stdout: NodeJS.WriteStream;
stdin: ReadableTTY;
stdout: WritableTTY;
trailing?: string;
yesChar?: string;
};

View File

@@ -1,5 +1,7 @@
import type { ReadableTTY } from '../../types';
export default async function readStandardInput(
stdin: NodeJS.ReadStream
stdin: ReadableTTY
): Promise<string> {
return new Promise<string>(resolve => {
setTimeout(() => resolve(''), 500);

View File

@@ -56,7 +56,7 @@ export default async function setupAndLink(
return { status: 'error', exitCode: 1, reason: 'PATH_IS_FILE' };
}
const link = await getLinkedProject(client, path);
const isTTY = process.stdout.isTTY;
const isTTY = client.stdin.isTTY;
const quiet = !isTTY;
let rootDirectory: string | null = null;
let sourceFilesOutsideRootDirectory = true;

View File

@@ -176,7 +176,7 @@ async function getVerificationTokenOutOfBand(client: Client, url: URL) {
output.log(
`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));
// If the pasted token begins with "saml_", then the `ssoUserId` was returned.

View File

@@ -1,4 +1,3 @@
import inquirer from 'inquirer';
import Client from '../client';
import error from '../output/error';
import listInput from '../input/list';
@@ -32,7 +31,7 @@ export default async function prompt(
choices.pop();
}
const choice = await listInput({
const choice = await listInput(client, {
message: 'Log in to Vercel',
choices,
});
@@ -44,22 +43,26 @@ export default async function prompt(
} else if (choice === 'bitbucket') {
result = await doBitbucketLogin(client, outOfBand, ssoUserId);
} 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);
} 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);
}
return result;
}
export async function readInput(message: string): Promise<string> {
export async function readInput(
client: Client,
message: string
): Promise<string> {
let input;
while (!input) {
try {
const { val } = await inquirer.prompt({
const { val } = await client.prompt({
type: 'input',
name: 'val',
message,

View File

@@ -1,41 +1,39 @@
import chalk from 'chalk';
import renderLink from './link';
import wait, { StopSpinner } from './wait';
import { Writable } from 'stream';
import type { WritableTTY } from '../../types';
export interface OutputOptions {
debug?: boolean;
}
export interface PrintOptions {
w?: Writable;
}
export interface LogOptions extends PrintOptions {
export interface LogOptions {
color?: typeof chalk;
}
export class Output {
stream: WritableTTY;
debugEnabled: boolean;
private spinnerMessage: string;
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.spinnerMessage = '';
this._spinner = null;
this.isTTY = process.stdout.isTTY || false;
}
isDebugEnabled = () => {
return this.debugEnabled;
};
print = (str: string, { w }: PrintOptions = { w: process.stderr }) => {
print = (str: string) => {
this.stopSpinner();
const stream: Writable = w || process.stderr;
stream.write(str);
this.stream.write(str);
};
log = (str: string, color = chalk.grey) => {
@@ -111,11 +109,17 @@ export class Output {
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
return;
}
if (this.isTTY) {
if (this.stream.isTTY) {
if (this._spinner) {
this._spinner.text = message;
} else {
this._spinner = wait(message, delay);
this._spinner = wait(
{
text: message,
stream: this.stream,
},
delay
);
}
} else {
this.print(`${message}\n`);
@@ -157,7 +161,3 @@ export class Output {
return promise;
};
}
export default function createOutput(opts?: OutputOptions) {
return new Output(opts);
}

View File

@@ -1,2 +1,2 @@
export { default, Output } from './create-output';
export { Output } from './create-output';
export { StopSpinner } from './wait';

View File

@@ -8,14 +8,19 @@ export interface StopSpinner {
}
export default function wait(
msg: string,
delay: number = 300,
_ora = ora
opts: ora.Options,
delay: number = 300
): 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(() => {
spinner = _ora(chalk.gray(msg));
spinner = ora(opts);
spinner.text = chalk.gray(text);
spinner.color = 'gray';
spinner.start();
}, 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
Object.defineProperty(stop, 'text', {
get() {
return msg;
return text;
},
set(v: string) {
msg = v;
text = v;
if (spinner) {
spinner.text = chalk.gray(v);
}
},
});
// @ts-ignore
process.once('nowExit', stop);
return stop;
}

View File

@@ -1,11 +1,10 @@
import chalk from 'chalk';
import { Response } from 'node-fetch';
import { emoji, EmojiLabel, prependEmoji } from './emoji';
import createOutput from './output';
import Client from './client';
import linkStyle from './output/link';
import { emoji, EmojiLabel, prependEmoji } from './emoji';
export default function printIndications(res: Response) {
const _output = createOutput();
export default function printIndications(client: Client, res: Response) {
const indications = new Set(['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)}`) +
newline;
}
_output.print(message + finalLink);
client.output.print(message + finalLink);
}
}
}

View File

@@ -4,7 +4,15 @@ export const config = {
};
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
response.headers.set('x-modified-edge', 'true');

View File

@@ -511,7 +511,20 @@ test(
testFixtureStdio('middleware-matchers', async (testPath: any) => {
await testPath(404, '/');
await testPath(404, '/another');
await testPath(200, '/about/page', 'middleware response');
await testPath(200, '/dashboard/home', 'middleware response');
await testPath(
200,
'/about/page',
'{"pathname":"/about/page","search":"","fromMiddleware":true}'
);
await testPath(
200,
'/dashboard/home',
'{"pathname":"/dashboard/home","search":"","fromMiddleware":true}'
);
await testPath(
200,
'/dashboard/home?a=b',
'{"pathname":"/dashboard/home","search":"?a=b","fromMiddleware":true}'
);
})
);

View File

@@ -1958,7 +1958,7 @@ test('ensure we render a prompt when deploying home directory', async t => {
t.is(exitCode, 0);
t.true(
stdout.includes(
stderr.includes(
'You are deploying your home directory. Do you want to continue? [y/N]'
)
);

View File

@@ -1,3 +1,6 @@
// Register Jest matcher extensions for CLI unit tests
import './matchers';
import chalk from 'chalk';
import { PassThrough } from 'stream';
import { createServer, Server } from 'http';
@@ -12,27 +15,43 @@ chalk.level = 0;
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 {
mockServer?: Server;
mockOutput: jest.Mock<void, Parameters<Output['print']>>;
private app: Express;
stdin!: MockStream;
stdout!: MockStream;
stderr!: MockStream;
scenario: Scenario;
mockServer?: Server;
private app: Express;
constructor() {
super({
argv: [],
// Gets populated in `startMockServer()`
apiUrl: '',
// Gets re-initialized for every test in `reset()`
argv: [],
authConfig: {},
stdin: new PassThrough(),
stdout: new PassThrough(),
output: new Output(),
config: {},
localConfig: {},
stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
output: new Output(new PassThrough()),
});
this.mockOutput = jest.fn();
this.app = express();
this.app.use(express.json());
@@ -57,33 +76,29 @@ export class MockClient extends Client {
}
reset() {
this.stdin = new PassThrough();
this.stdin.isTTY = true;
this.stdin = new MockStream();
this.stdout = new PassThrough();
this.stdout.isTTY = true;
this.stdout = new MockStream();
this.stdout.setEncoding('utf8');
this.stdout.end = () => {};
this.stdout.pause();
this.output = new Output();
this.mockOutput = jest.fn();
this.output.print = s => {
return this.mockOutput(s);
};
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.authConfig = {};
this.config = {};
this.localConfig = {};
// Just make this one silent
this.output.spinner = () => {};
this.scenario = Router();
this.output.isTTY = true;
}
get outputBuffer() {
return this.mockOutput.mock.calls.map(c => c[0]).join('');
}
async startMockServer() {

View 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.");
}

View File

@@ -0,0 +1 @@
export * from './to-output';

View 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();
});
}

View File

@@ -10,38 +10,38 @@ import { useUser } from '../../mocks/user';
describe('deploy', () => {
it('should reject deploying a single file', async () => {
client.setArgv('deploy', __filename);
const exitCode = await deploy(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
`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 () => {
client.setArgv('deploy', __filename, join(__dirname, 'inspect.test.ts'));
const exitCode = await deploy(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
`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 () => {
client.setArgv('deploy', 'does-not-exists');
const exitCode = await deploy(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
`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 () => {
client.setArgv('deploy', __dirname, '--prebuilt');
const exitCode = await deploy(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
'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 () => {
@@ -56,14 +56,14 @@ describe('deploy', () => {
});
client.setArgv('deploy', cwd, '--prebuilt', '--prod');
const exitCode = await deploy(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
'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".' +
' Please run `vercel --prebuilt`.\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 () => {
@@ -78,14 +78,14 @@ describe('deploy', () => {
});
client.setArgv('deploy', cwd, '--prebuilt');
const exitCode = await deploy(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
'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".' +
' Please run `vercel --prebuilt --prod`.\n' +
'Learn More: https://vercel.link/prebuilt-environment-mismatch\n'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should reject deploying "version: 1"', async () => {
@@ -94,11 +94,11 @@ describe('deploy', () => {
[fileNameSymbol]: 'vercel.json',
version: 1,
};
const exitCode = await deploy(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
'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 () => {
@@ -108,10 +108,10 @@ describe('deploy', () => {
// @ts-ignore
version: {},
};
const exitCode = await deploy(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
const exitCodePromise = deploy(client);
await expect(client.stderr).toOutput(
'Error! The `version` property inside your vercel.json file must be a number.\n'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
});

View File

@@ -19,8 +19,12 @@ describe('env', () => {
name: 'vercel-env-pull',
});
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
const exitCode = await env(client);
expect(exitCode, client.outputBuffer).toEqual(0);
const exitCodePromise = env(client);
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'));
@@ -39,8 +43,12 @@ describe('env', () => {
name: 'vercel-env-pull',
});
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
const exitCode = await env(client);
expect(exitCode, client.outputBuffer).toEqual(0);
const exitCodePromise = env(client);
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'));
@@ -61,8 +69,12 @@ describe('env', () => {
});
client.setArgv('env', 'pull', 'other.env', '--yes', '--cwd', cwd);
const exitCode = await env(client);
expect(exitCode, client.outputBuffer).toEqual(0);
const exitCodePromise = env(client);
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'));

View File

@@ -10,11 +10,9 @@ describe('inspect', () => {
client.setArgv('inspect', deployment.url);
const exitCode = await inspect(client);
expect(exitCode).toEqual(0);
expect(
client.mockOutput.mock.calls[0][0].startsWith(
`> Fetched deployment "${deployment.url}" in ${user.username}`
)
).toBeTruthy();
await expect(client.stderr).toOutput(
`> Fetched deployment "${deployment.url}" in ${user.username}`
);
});
it('should print error when deployment not found', async () => {
@@ -23,7 +21,7 @@ describe('inspect', () => {
client.setArgv('inspect', 'bad.com');
const exitCode = await inspect(client);
expect(exitCode).toEqual(1);
expect(client.outputBuffer).toEqual(
await expect(client.stderr).toOutput(
`Error! Failed to find deployment "bad.com" in ${user.username}\n`
);
});

View File

@@ -5,22 +5,45 @@ import { useUser } from '../../mocks/user';
describe('login', () => {
it('should not allow the `--token` flag', async () => {
client.setArgv('login', '--token', 'foo');
const exitCode = await login(client);
expect(exitCode).toEqual(2);
expect(client.outputBuffer).toEqual(
const exitCodePromise = login(client);
await expect(client.stderr).toOutput(
'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 () => {
const user = useUser();
client.setArgv('login', user.email);
const exitCode = await login(client);
expect(exitCode).toEqual(0);
expect(
client.outputBuffer.includes(
const exitCodePromise = login(client);
await expect(client.stderr).toOutput(
`Success! Email authentication complete for ${user.email}`
);
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}`
)
).toEqual(true);
);
await expect(exitCodePromise).resolves.toEqual(0);
});
});
});

View File

@@ -18,8 +18,17 @@ describe('pull', () => {
name: 'vercel-pull-next',
});
client.setArgv('pull', cwd);
const exitCode = await pull(client);
expect(exitCode, client.outputBuffer).toEqual(0);
const exitCodePromise = pull(client);
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(
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 () => {
try {
process.stdout.isTTY = undefined;
client.stdin.isTTY = false;
const cwd = setupFixture('vercel-pull-unlinked');
useUser();
useTeams('team_dummy');
const cwd = setupFixture('vercel-pull-unlinked');
useUser();
useTeams('team_dummy');
client.setArgv('pull', cwd);
const exitCode = await pull(client);
expect(exitCode, client.outputBuffer).toEqual(1);
expect(client.outputBuffer).toMatch(
/Command `vercel pull` requires confirmation. Use option "--yes" to confirm./gm
);
} finally {
process.stdout.isTTY = true;
}
client.setArgv('pull', cwd);
const exitCodePromise = pull(client);
await expect(client.stderr).toOutput(
'Command `vercel pull` requires confirmation. Use option "--yes" to confirm.'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should fail without message to pull without a link and with --env', async () => {
@@ -54,12 +58,11 @@ describe('pull', () => {
useTeams('team_dummy');
client.setArgv('pull', cwd, '--yes');
const exitCode = await pull(client);
expect(exitCode, client.outputBuffer).toEqual(1);
expect(client.outputBuffer).not.toMatch(
/Command `vercel pull` requires confirmation. Use option "--yes" to confirm./gm
const exitCodePromise = pull(client);
await expect(client.stderr).not.toOutput(
'Command `vercel pull` requires confirmation. Use option "--yes" to confirm.'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should handle pulling with env vars (headless mode)', async () => {
@@ -81,8 +84,17 @@ describe('pull', () => {
name: 'vercel-pull-next',
});
client.setArgv('pull', cwd);
const exitCode = await pull(client);
expect(exitCode, client.outputBuffer).toEqual(0);
const exitCodePromise = pull(client);
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'));
expect(config).toMatchInlineSnapshot(`
@@ -108,8 +120,17 @@ describe('pull', () => {
name: 'vercel-pull-next',
});
client.setArgv('pull', '--environment=preview', cwd);
const exitCode = await pull(client);
expect(exitCode).toEqual(0);
const exitCodePromise = pull(client);
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(
path.join(cwd, '.vercel', '.env.preview.local')
@@ -130,8 +151,17 @@ describe('pull', () => {
name: 'vercel-pull-next',
});
client.setArgv('pull', '--environment=production', cwd);
const exitCode = await pull(client);
expect(exitCode).toEqual(0);
const exitCodePromise = pull(client);
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(
path.join(cwd, '.vercel', '.env.production.local')

View File

@@ -14,14 +14,14 @@ describe('whoami', () => {
const user = useUser();
const exitCode = await whoami(client);
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 () => {
const user = useUser();
client.output.isTTY = false;
client.stdout.isTTY = false;
const exitCode = await whoami(client);
expect(exitCode).toEqual(0);
expect(client.outputBuffer).toEqual(`${user.username}\n`);
await expect(client.stdout).toOutput(`${user.username}\n`);
});
});

View File

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

View File

@@ -0,0 +1,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);
});
});

View File

@@ -22,10 +22,7 @@ describe('getRemoteUrl', () => {
client.output.debugEnabled = true;
const data = await getRemoteUrl(join(dir, 'git/config'), client.output);
expect(data).toBeNull();
expect(
client.outputBuffer.includes('Error while parsing repo data'),
'Debug message was not found'
).toBeTruthy();
await expect(client.stderr).toOutput('Error while parsing repo data');
});
});

View File

@@ -1,18 +1,15 @@
import { join, sep } from 'path';
// @ts-ignore - Missing types for "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 { client } from '../../mocks/client';
const output = createOutput({ debug: false });
const prefix = `${join(__dirname, '../../fixtures/unit')}${sep}`;
const base = (path: string) => path.replace(prefix, '');
const fixture = (name: string) => join(prefix, name);
const getStaticFiles = async (dir: string) => {
const files = await getStaticFiles_(dir, {
output,
});
const files = await getStaticFiles_(dir, client);
return normalizeWindowsPaths(files);
};

View File

@@ -27,14 +27,13 @@ describe('editProjectSettings', () => {
installCommand: null,
outputDirectory: null,
});
expect(client.mockOutput.mock.calls.length).toBe(5);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/No framework detected. Default Project Settings:/
await expect(client.stderr).toOutput(
'No framework detected. Default Project Settings:'
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Development Command/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(/Install Command/);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Output Directory/);
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');
});
});
@@ -55,14 +54,13 @@ describe('editProjectSettings', () => {
null
);
expect(settings).toStrictEqual({ ...projectSettings, framework: null });
expect(client.mockOutput.mock.calls.length).toBe(5);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/No framework detected. Default Project Settings:/
await expect(client.stderr).toOutput(
'No framework detected. Default Project Settings:'
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Development Command/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(/Install Command/);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Output Directory/);
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');
});
});
@@ -82,18 +80,15 @@ describe('editProjectSettings', () => {
true,
null
);
expect(client.mockOutput.mock.calls.length).toBe(5);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/Auto-detected Project Settings/
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Development Command/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(/Install Command/);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Output Directory/);
expect(settings).toStrictEqual({
...projectSettings,
framework: nextJSFramework.slug,
});
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');
});
});
@@ -121,26 +116,20 @@ describe('editProjectSettings', () => {
true,
overrides
);
expect(client.mockOutput.mock.calls.length).toBe(9);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/Local settings detected in vercel.json:/
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command:/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Ignore Command:/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(
/Development Command:/
);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Framework:/);
expect(client.mockOutput.mock.calls[5][0]).toMatch(/Install Command:/);
expect(client.mockOutput.mock.calls[6][0]).toMatch(/Output Directory:/);
expect(client.mockOutput.mock.calls[7][0]).toMatch(
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
);
expect(client.mockOutput.mock.calls[8][0]).toMatch(
/Auto-detected Project Settings/
);
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');
});
});
@@ -161,25 +150,20 @@ describe('editProjectSettings', () => {
true,
overrides
);
expect(client.mockOutput.mock.calls.length).toBe(9);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/Local settings detected in vercel.json:/
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command:/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Ignore Command:/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(
/Development Command:/
);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Framework:/);
expect(client.mockOutput.mock.calls[5][0]).toMatch(/Install Command:/);
expect(client.mockOutput.mock.calls[6][0]).toMatch(/Output Directory:/);
expect(client.mockOutput.mock.calls[7][0]).toMatch(
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
);
expect(client.mockOutput.mock.calls[8][0]).toMatch(
/Auto-detected Project Settings/
);
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');
});
});
@@ -200,26 +184,20 @@ describe('editProjectSettings', () => {
true,
overrides
);
expect(client.mockOutput.mock.calls.length).toBe(9);
expect(client.mockOutput.mock.calls[0][0]).toMatch(
/Local settings detected in vercel.json:/
);
expect(client.mockOutput.mock.calls[1][0]).toMatch(/Build Command:/);
expect(client.mockOutput.mock.calls[2][0]).toMatch(/Ignore Command:/);
expect(client.mockOutput.mock.calls[3][0]).toMatch(
/Development Command:/
);
expect(client.mockOutput.mock.calls[4][0]).toMatch(/Framework:/);
expect(client.mockOutput.mock.calls[5][0]).toMatch(/Install Command:/);
expect(client.mockOutput.mock.calls[6][0]).toMatch(/Output Directory:/);
expect(client.mockOutput.mock.calls[7][0]).toMatch(
/Merging default Project Settings for Svelte. Previously listed overrides are prioritized./
);
expect(client.mockOutput.mock.calls[8][0]).toMatch(
/Auto-detected Project Settings/
);
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');
});
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "12.0.3",
"version": "12.0.4",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -42,7 +42,8 @@
]
},
"dependencies": {
"@vercel/build-utils": "4.2.1",
"@vercel/build-utils": "5.0.0",
"@vercel/routing-utils": "1.13.5",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

@@ -1,9 +1,9 @@
import {
import type {
Builder,
BuilderFunctions,
ProjectSettings,
} 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';

1
packages/fs-detectors/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/dist

View File

@@ -0,0 +1,5 @@
/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

View 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"
}
}

View File

@@ -1,16 +1,16 @@
import minimatch from 'minimatch';
import { valid as validSemver } from 'semver';
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 {
import type {
PackageJson,
Builder,
Config,
BuilderFunctions,
ProjectSettings,
} from './types';
import { isOfficialRuntime } from './';
} from '@vercel/build-utils';
import { isOfficialRuntime } from './is-official-runtime';
const slugToFramework = new Map<string | null, Framework>(
frameworkList.map(f => [f.slug, f])
);

View File

@@ -5,7 +5,7 @@ import type {
BuilderFunctions,
PackageJson,
ProjectSettings,
} from './types';
} from '@vercel/build-utils';
interface Metadata {
plugins: string[];

View File

@@ -1,4 +1,4 @@
import { Framework, FrameworkDetectionItem } from '@vercel/frameworks';
import type { Framework, FrameworkDetectionItem } from '@vercel/frameworks';
import { DetectorFilesystem } from './detectors/filesystem';
interface BaseFramework {

View 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';

View 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);
};

View File

@@ -6,8 +6,8 @@ import type { Framework } from '@vercel/frameworks';
* This list is designed to work with the @see {@link detectFramework} function.
*
* @example
* import { monorepoManagers as frameworkList } from '@vercel/build-utils'
* import { detectFramework } from '@vercel/build-utils'
* import { monorepoManagers as frameworkList } from '@vercel/fs-detectors'
* import { detectFramework } from '@vercel/fs-detectors'
*
* const fs = new GitDetectorFilesystem(...)
* detectFramwork({ fs, frameworkList }) // returns the 'slug' field if detected, otherwise null

View File

@@ -3,7 +3,7 @@ import { DetectorFilesystem } from '../detectors/filesystem';
type GlobFs = typeof fs;
function normalizePath(path: string) {
function removeWindowsPrefix(path: string) {
// on windows, this will return a path like
// D:/c/package.json
// 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
): void => {
_fs
.readdir(normalizePath(String(path)))
.readdir(removeWindowsPrefix(String(path)))
.then(stats =>
callback(
null,
@@ -36,7 +36,7 @@ export function getGlobFs(_fs: DetectorFilesystem): GlobFs {
) => void
): void => {
_fs
.isFile(normalizePath(String(path)))
.isFile(removeWindowsPrefix(String(path)))
.then(isPathAFile => {
callback(null, {
ino: 0,

View File

@@ -3,8 +3,7 @@ import yaml from 'js-yaml';
import glob from 'glob';
import { DetectorFilesystem } from '../detectors/filesystem';
import { Workspace } from './get-workspaces';
import { getGlobFs } from '../fs/get-glob-fs';
import { normalizePath } from '../fs/normalize-path';
import { getGlobFs } from './get-glob-fs';
const posixPath = _path.posix;
@@ -56,6 +55,9 @@ type PnpmWorkspaces = {
packages?: string[];
};
const isWin = process.platform === 'win32';
const normalizePath = (p: string) => (isWin ? p.replace(/\\/g, '/') : p);
async function getPackagePaths(
packages: string[],
fs: DetectorFilesystem

View File

@@ -36,7 +36,7 @@ export async function getWorkspaces({
const childDirectories = directoryContents.filter(
stat => stat.type === 'dir'
);
return (
await Promise.all(
childDirectories.map(childDirectory =>

View File

@@ -6,8 +6,8 @@ import type { Framework } from '@vercel/frameworks';
* This list is designed to work with the @see {@link detectFramework} function.
*
* @example
* import { workspaceManagers as frameworkList } from '@vercel/build-utils'
* import { detectFramework } from '@vercel/build-utils'
* import { workspaceManagers as frameworkList } from '@vercel/fs-detectors'
* import { detectFramework } from '@vercel/fs-detectors'
*
* const fs = new GitDetectorFilesystem(...)
* detectFramwork({ fs, frameworkList }) // returns the 'slug' field if detected, otherwise null

View File

@@ -0,0 +1 @@
now.json

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end('hello from api/date.js');
};

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end('hello from api/date/index.js');
};

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end('hello from api/index.js');
};

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"build": "mkdir -p public && echo 'hello from index.txt' > public/index.txt"
}
}

View File

@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

@@ -0,0 +1,782 @@
{
"name": "21-npm-workspaces",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "1.0.0",
"workspaces": [
"a",
"b"
]
},
"a": {
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"debug": "^4.3.2"
}
},
"b": {
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"cowsay": "^1.5.0"
}
},
"node_modules/a": {
"resolved": "a",
"link": true
},
"node_modules/ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"engines": {
"node": ">=4"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/b": {
"resolved": "b",
"link": true
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"engines": {
"node": ">=6"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dependencies": {
"ansi-regex": "^5.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/cowsay": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.5.0.tgz",
"integrity": "sha512-8Ipzr54Z8zROr/62C8f0PdhQcDusS05gKTS87xxdji8VbWefWly0k8BwGK7+VqamOrkv3eGsCkPtvlHzrhWsCA==",
"dependencies": {
"get-stdin": "8.0.0",
"string-width": "~2.1.1",
"strip-final-newline": "2.0.0",
"yargs": "15.4.1"
},
"bin": {
"cowsay": "cli.js",
"cowthink": "cli.js"
},
"engines": {
"node": ">= 4"
}
},
"node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-stdin": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"engines": {
"node": ">=4"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"engines": {
"node": ">=8"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"node_modules/string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dependencies": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dependencies": {
"ansi-regex": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"engines": {
"node": ">=6"
}
},
"node_modules/which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dependencies": {
"ansi-regex": "^5.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dependencies": {
"ansi-regex": "^5.0.0"
},
"engines": {
"node": ">=8"
}
}
},
"dependencies": {
"a": {
"version": "file:a",
"requires": {
"debug": "^4.3.2"
}
},
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"b": {
"version": "file:b",
"requires": {
"cowsay": "^1.5.0"
}
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^5.0.0"
}
}
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"cowsay": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.5.0.tgz",
"integrity": "sha512-8Ipzr54Z8zROr/62C8f0PdhQcDusS05gKTS87xxdji8VbWefWly0k8BwGK7+VqamOrkv3eGsCkPtvlHzrhWsCA==",
"requires": {
"get-stdin": "8.0.0",
"string-width": "~2.1.1",
"strip-final-newline": "2.0.0",
"yargs": "15.4.1"
}
},
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": {
"ms": "2.1.2"
}
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"get-stdin": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg=="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"requires": {
"p-locate": "^4.1.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"requires": {
"p-limit": "^2.2.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "^3.0.0"
}
},
"strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^5.0.0"
}
}
}
},
"y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"requires": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^5.0.0"
}
}
}
},
"yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "21-npm-workspaces",
"version": "1.0.0",
"workspaces": [
"a",
"b"
]
}

View File

@@ -0,0 +1,15 @@
{
"name": "22-pnpm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "mkdir -p public && (printf \"pnpm version: \" && pnpm -v) > public/index.txt"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"once": "^1.4.0"
}
}

View File

@@ -0,0 +1,19 @@
lockfileVersion: 5.3
specifiers:
once: ^1.4.0
dependencies:
once: 1.4.0
packages:
/once/1.4.0:
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
dependencies:
wrappy: 1.0.2
dev: false
/wrappy/1.0.2:
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
dev: false

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@vercel/static-build" }],
"probes": [
{
"path": "/",
"mustContain": "pnpm version: 6",
"logMustContain": "pnpm run build"
}
]
}

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