Compare commits

..

23 Commits

Author SHA1 Message Date
Sean Massa
d2f8d178f7 Publish Stable
- @vercel/build-utils@5.0.2
 - vercel@27.1.0
 - @vercel/client@12.1.1
 - @vercel/fs-detectors@2.0.0
 - @vercel/go@2.0.6
 - @vercel/hydrogen@0.0.3
 - @vercel/next@3.1.5
 - @vercel/node@2.4.3
 - @vercel/python@3.0.6
 - @vercel/redwood@1.0.7
 - @vercel/remix@1.0.8
 - @vercel/ruby@1.3.14
 - @vercel/static-build@1.0.6
2022-07-12 15:34:28 -05:00
Nathan Rajlich
f9a747764c [remix] Fix isMonorepo check (#8142)
`config.projectSettings` is not available when running `@vercel/remix` directly (i.e. when not using `vc build`), so calculate the correct root directory value based on the `workPath` relative to the `repoRootPath` value.
2022-07-12 20:10:51 +00:00
Matthew Stanciu
27d80f13cd [cli] Add ability to connect Git provider repo (#8100)
This PR adds a new subcommand `vc git`, which allows you to create a Git provider repository from your local `.git` config to your Vercel project.

Usage:

- `vc git connect` – connects a Git provider repository
- `vc git disconnect` – disconnects a Git provider repository

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-12 17:55:40 +00:00
Nathan Rajlich
8c668c925d [fs-detectors] Remove "solidstart" framework from FS API v2 detection (#8134)
"solidstart" framework doesn't need to use the FS API v2 Builder anymore since:

  1) Newer versions output Build Output API v3
  2) Older versions that output FS API v2 will be handled by the compatbility shim that was added in https://github.com/vercel/vercel/pull/7690
2022-07-12 17:03:00 +00:00
Nathan Rajlich
4b1b33c143 [cli] Add createdAt to project settings in vc pull (#8138)
The `createdAt` property is checked in the `detectBuilders()` function, so it needs to be present in the local copy of the project settings.
2022-07-12 08:12:32 +00:00
Gal Schlezinger
a8d4147554 [next] Add EdgeRuntime provider environment variable to the builder (#8130)
This will enable Next.js to compile and set `vercel` as the edge runtime provider (`EdgeRuntime` global),
which can allow different libraries/customers to have different code running depends on the runtime environment (`edge-runtime` vs `vercel`).

### Related Issues

- https://github.com/vercel/next.js/pull/38331

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

The Next.js feature was not merged yet, so it still can't be tested.

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-12 06:33:14 +00:00
Gal Schlezinger
09339f494d [next][build-utils] allow to declare assets in EdgeFunction constructor (#8127)
There's a [Next.js PR](https://github.com/vercel/next.js/pull/38492) to include binary assets in Edge Functions. This PR enables to declare assets in the `EdgeFunction` constructor.

This is a configuration level setting, compared to WebAssembly modules which the `middleware-manifest.json` converts into `import` statements. This is because the asset generation is done in Webpack and it is not dynamic, it's a static string. We can use AST to replace it but we're risking in:

* worse performance
* accidentally replacing a string that isn't an asset
2022-07-11 23:13:21 +00:00
Steven
ee4d772ae9 [tests] Add retry to npm tests (#8133)
These tests fail occasionally on GH Actions due to flakey network IO, so this PR adds retry logic.

https://github.com/vercel/vercel/runs/7289659634?check_suite_focus=true#step:13:328
2022-07-11 21:39:10 +00:00
Matthew Stanciu
61e8103404 [tests] Fix flaky Git meta test (#8132)
A test I wrote for a previous PR (https://github.com/vercel/vercel/pull/8112) was failing once in ~every 5 runs. This PR makes the test reliable.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-11 21:01:16 +00:00
Steven
fb4f477325 Publish Stable
- vercel@27.0.2
 - @vercel/fs-detectors@1.0.2
 - @vercel/node@2.4.2
2022-07-11 16:21:30 -04:00
P.B. To
016bff848e [fs-detectors] process detectFramework in parallel (#8128)
The Vercel API's `detect-framework` API contacts GitHub's API using its own file adapter. 

However, currently the `detectFramework` function in the Vercel CLI (on which the API depends) calls the file system adapter in series using a `for` loop. This results in large amounts of lag when in a networked situation such as calling GitHub's API as each API call takes a couple hundred milliseconds.

I propose changing `detectFramework` to process the directories it scans in parallel.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2022-07-11 20:16:04 +00:00
Steven
183e411f7c [cli] Remove DEBUG=corepack env var (#8131)
This debug log was originally added in #7871 because corepack has no output by default. In particular, it was nice to see the first deployment was not stalled when the package manager is being installed.

That being said, this gets noisy really fast because cache detections also print a log line.
For example, here's a deployment that prints 3 times:

```
Detected ENABLE_EXPERIMENTAL_COREPACK=1 and "npm@8.10.0" in package.json
Running "install" command: `npm install --prefix=.. --no-audit --engine-strict=false`...
2022-07-11T18:27:00.696Z corepack Reusing npm@8.10.0
356 packages are looking for funding
Running "npm run vercel-build"
2022-07-11T18:27:06.664Z corepack Reusing npm@8.10.0
> front@0.0.0 vercel-build
> npm run buildonly && npm run build:rss
2022-07-11T18:27:07.088Z corepack Reusing npm@8.10.0
> front@0.0.0 buildonly
> next build
```

I think its best to let users add this env var themselves if they want to debug what corepack is doing, so this PR removes that environment variable.
2022-07-11 19:31:04 +00:00
Sean Massa
070e300148 [node] add links to edge error messages (#8048)
Add links to some Edge errors.

---

Follow up to: https://github.com/vercel/vercel/pull/8007
2022-07-11 18:50:15 +00:00
Steven
cbdf9b4a88 [cli] Fix beta label (#8129)
This PR is a follow up to #7991 which incorrectly used a the empty string instead of empty array

- Maybe related to #8125
2022-07-11 18:05:36 +00:00
Steven
ec9b55dc81 Publish Stable
- vercel@27.0.1
 - @vercel/remix@1.0.7
2022-07-10 15:04:09 -04:00
Leon Salsiccia
06829bc21a [remix] fix monorepo support (#8077)
Co-authored-by: Steven <steven@ceriously.com>
2022-07-10 13:28:39 -04:00
Matthew Stanciu
628071f659 [cli] Debug log error messages in create-git-meta (#8112)
This is a follow-up to #8094 which debug logs errors.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-08 19:30:28 +00:00
Matthew Stanciu
5a7461dfe3 [cli] Explicitly use vc project vs. vc projects (#8113)
This is a follow-up to #8091 which:

- Makes `vc project` the default command, with `vc projects` aliased to `vc project` (previously it was not clear in the code which one was the "real" command)
- Makes some helper names for `ls` more specific

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-08 18:56:35 +00:00
Sean Massa
599f8f675c [tests] remove commented code (#8109)
Removes a leftover commented line.
2022-07-08 16:52:55 +00:00
Steven
0a8bc494fc [tests] Try building with ENABLE_VC_BUILD (#8110)
This will dogfood `vc build` which happens to improve this deployment time from 4 min down to 2 min.
2022-07-08 12:30:03 -04:00
Lee Robinson
34e008f42e [cli] Remove beta warning for vercel build (#7991)
This removes the `(beta)` output from `vercel build` as we move towards stability.

![image](https://user-images.githubusercontent.com/9113740/174356399-65f3d6bb-a241-49c8-9edb-167b25d6fa44.png)
2022-07-08 12:12:22 +00:00
Matthew Stanciu
037633b3f1 [cli] Refactor vc project (#8091)
Since the `vc project` command is about to be expanded with 2 new subcommands, I think it makes sense to do a little bit of refactoring. The current `vc project` command is all in one file, with the logic for subcommands nested within if statements. I think the structure of `vc project` should look more like `vc env`, which is consistent with how commands with subcommands look throughout the rest of the codebase.

This PR moves the logic for the `project` subcommands into their own files.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-08 01:34:13 +00:00
Matthew Stanciu
1a6f3c0270 [cli] Fix vc deploy erroring when .git is corrupt (#8094)
### Related Issues

- Fixes https://github.com/vercel/customer-issues/issues/597

`vc deploy` currently fails with a confusing error if the user has a corrupt `.git` directory. This is caused by `create-git-meta` improperly handling the scenario where it detects a `.git` directory but cannot find git information. This PR handles this error.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-08 01:07:43 +00:00
80 changed files with 1802 additions and 478 deletions

View File

@@ -5,7 +5,8 @@
"description": "API for the vercel/vercel repo", "description": "API for the vercel/vercel repo",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"vercel-build": "node ../utils/run.js build all" "//TODO": "We should add this pkg to yarn workspaces",
"vercel-build": "cd .. && yarn install && yarn vercel-build"
}, },
"dependencies": { "dependencies": {
"@sentry/node": "5.11.1", "@sentry/node": "5.11.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/build-utils", "name": "@vercel/build-utils",
"version": "5.0.1", "version": "5.0.2",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.js", "types": "./dist/index.d.js",

View File

@@ -33,6 +33,11 @@ export class EdgeFunction {
*/ */
envVarsInUse?: string[]; envVarsInUse?: string[];
/**
* Extra binary files to be included in the edge function
*/
assets?: { name: string; path: string }[];
constructor(params: Omit<EdgeFunction, 'type'>) { constructor(params: Omit<EdgeFunction, 'type'>) {
this.type = 'EdgeFunction'; this.type = 'EdgeFunction';
this.name = params.name; this.name = params.name;
@@ -40,5 +45,6 @@ export class EdgeFunction {
this.entrypoint = params.entrypoint; this.entrypoint = params.entrypoint;
this.files = params.files; this.files = params.files;
this.envVarsInUse = params.envVarsInUse; this.envVarsInUse = params.envVarsInUse;
this.assets = params.assets;
} }
} }

View File

@@ -1,6 +1,7 @@
import ms from 'ms'; import ms from 'ms';
import path from 'path'; import path from 'path';
import fs, { readlink } from 'fs-extra'; import fs, { readlink } from 'fs-extra';
import retry from 'async-retry';
import { strict as assert, strictEqual } from 'assert'; import { strict as assert, strictEqual } from 'assert';
import { createZip } from '../src/lambda'; import { createZip } from '../src/lambda';
import { getSupportedNodeVersion } from '../src/fs/node-version'; import { getSupportedNodeVersion } from '../src/fs/node-version';
@@ -494,28 +495,43 @@ it('should only invoke `runNpmInstall()` once per `package.json` file (serial)',
const meta: Meta = {}; const meta: Meta = {};
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api'); const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const apiDir = path.join(fixture, 'api'); const apiDir = path.join(fixture, 'api');
const run1 = await runNpmInstall(apiDir, [], undefined, meta); const retryOpts = { maxRetryTime: 1000 };
expect(run1).toEqual(true); let run1, run2, run3;
expect( await retry(async () => {
(meta.runNpmInstallSet as Set<string>).has( run1 = await runNpmInstall(apiDir, [], undefined, meta);
path.join(fixture, 'package.json') expect(run1).toEqual(true);
) expect(
).toEqual(true); (meta.runNpmInstallSet as Set<string>).has(
const run2 = await runNpmInstall(apiDir, [], undefined, meta); path.join(fixture, 'package.json')
expect(run2).toEqual(false); )
const run3 = await runNpmInstall(fixture, [], undefined, meta); ).toEqual(true);
expect(run3).toEqual(false); }, retryOpts);
await retry(async () => {
run2 = await runNpmInstall(apiDir, [], undefined, meta);
expect(run2).toEqual(false);
}, retryOpts);
await retry(async () => {
run3 = await runNpmInstall(fixture, [], undefined, meta);
expect(run3).toEqual(false);
}, retryOpts);
}); });
it('should only invoke `runNpmInstall()` once per `package.json` file (parallel)', async () => { it('should only invoke `runNpmInstall()` once per `package.json` file (parallel)', async () => {
const meta: Meta = {}; const meta: Meta = {};
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api'); const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const apiDir = path.join(fixture, 'api'); const apiDir = path.join(fixture, 'api');
const [run1, run2, run3] = await Promise.all([ let results: [boolean, boolean, boolean] | undefined;
runNpmInstall(apiDir, [], undefined, meta), await retry(
runNpmInstall(apiDir, [], undefined, meta), async () => {
runNpmInstall(fixture, [], undefined, meta), results = await Promise.all([
]); runNpmInstall(apiDir, [], undefined, meta),
runNpmInstall(apiDir, [], undefined, meta),
runNpmInstall(fixture, [], undefined, meta),
]);
},
{ maxRetryTime: 3000 }
);
const [run1, run2, run3] = results || [];
expect(run1).toEqual(true); expect(run1).toEqual(true);
expect(run2).toEqual(false); expect(run2).toEqual(false);
expect(run3).toEqual(false); expect(run3).toEqual(false);

View File

@@ -1,6 +1,6 @@
{ {
"name": "vercel", "name": "vercel",
"version": "27.0.0", "version": "27.1.0",
"preferGlobal": true, "preferGlobal": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "The command-line interface for Vercel", "description": "The command-line interface for Vercel",
@@ -42,16 +42,16 @@
"node": ">= 14" "node": ">= 14"
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"@vercel/go": "2.0.5", "@vercel/go": "2.0.6",
"@vercel/hydrogen": "0.0.2", "@vercel/hydrogen": "0.0.3",
"@vercel/next": "3.1.4", "@vercel/next": "3.1.5",
"@vercel/node": "2.4.1", "@vercel/node": "2.4.3",
"@vercel/python": "3.0.5", "@vercel/python": "3.0.6",
"@vercel/redwood": "1.0.6", "@vercel/redwood": "1.0.7",
"@vercel/remix": "1.0.6", "@vercel/remix": "1.0.8",
"@vercel/ruby": "1.3.13", "@vercel/ruby": "1.3.14",
"@vercel/static-build": "1.0.5", "@vercel/static-build": "1.0.6",
"update-notifier": "5.1.0" "update-notifier": "5.1.0"
}, },
"devDependencies": { "devDependencies": {
@@ -96,9 +96,9 @@
"@types/which": "1.3.2", "@types/which": "1.3.2",
"@types/write-json-file": "2.2.1", "@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0", "@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.1.0", "@vercel/client": "12.1.1",
"@vercel/frameworks": "1.1.0", "@vercel/frameworks": "1.1.0",
"@vercel/fs-detectors": "1.0.1", "@vercel/fs-detectors": "2.0.0",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2", "@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2", "@zeit/source-map-support": "0.6.2",

View File

@@ -336,8 +336,7 @@ export default async function main(client: Client): Promise<number> {
const buildResults: Map<Builder, BuildResult> = new Map(); const buildResults: Map<Builder, BuildResult> = new Map();
const overrides: PathOverride[] = []; const overrides: PathOverride[] = [];
const repoRootPath = cwd; const repoRootPath = cwd;
const rootPackageJsonPath = repoRootPath || workPath; const corepackShimDir = await initCorepack({ repoRootPath });
const corepackShimDir = await initCorepack({ cwd, rootPackageJsonPath });
for (const build of builds) { for (const build of builds) {
if (typeof build.src !== 'string') continue; if (typeof build.src !== 'string') continue;

View File

@@ -15,6 +15,7 @@ export const help = () => `
)} )}
dev Start a local development server dev Start a local development server
env Manages the Environment Variables for your current Project env Manages the Environment Variables for your current Project
git Manage Git provider repository for your current Project
init [example] Initialize an example project init [example] Initialize an example project
ls | list [app] Lists deployments ls | list [app] Lists deployments
inspect [id] Displays information related to a deployment inspect [id] Displays information related to a deployment

View File

@@ -64,7 +64,7 @@ import { help } from './args';
import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks'; import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks';
import parseTarget from '../../util/deploy/parse-target'; import parseTarget from '../../util/deploy/parse-target';
import getPrebuiltJson from '../../util/deploy/get-prebuilt-json'; import getPrebuiltJson from '../../util/deploy/get-prebuilt-json';
import { createGitMeta } from '../../util/deploy/create-git-meta'; import { createGitMeta } from '../../util/create-git-meta';
export default async (client: Client) => { export default async (client: Client) => {
const { output } = client; const { output } = client;

View File

@@ -0,0 +1,168 @@
import chalk from 'chalk';
import { join } from 'path';
import { Org, Project } from '../../types';
import Client from '../../util/client';
import { parseGitConfig, pluckRemoteUrl } from '../../util/create-git-meta';
import confirm from '../../util/input/confirm';
import { Output } from '../../util/output';
import link from '../../util/output/link';
import { getCommandName } from '../../util/pkg-name';
import {
connectGitProvider,
disconnectGitProvider,
formatProvider,
parseRepoUrl,
} from '../../util/projects/connect-git-provider';
import validatePaths from '../../util/validate-paths';
export default async function connect(
client: Client,
argv: any,
args: string[],
project: Project | undefined,
org: Org | undefined
) {
const { output } = client;
const confirm = Boolean(argv['--confirm']);
if (args.length !== 0) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('project connect')}`
)}`
);
return 2;
}
if (!project || !org) {
output.error(
`Can't find \`org\` or \`project\`. Make sure your current directory is linked to a Vercel projet by running ${getCommandName(
'link'
)}.`
);
return 1;
}
let paths = [process.cwd()];
const validate = await validatePaths(client, paths);
if (!validate.valid) {
return validate.exitCode;
}
const { path } = validate;
const gitProviderLink = project.link;
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
// get project from .git
const gitConfigPath = join(path, '.git/config');
const gitConfig = await parseGitConfig(gitConfigPath, output);
if (!gitConfig) {
output.error(
`No local git repo found. Run ${chalk.cyan(
'`git clone <url>`'
)} to clone a remote Git repository first.`
);
return 1;
}
const remoteUrl = pluckRemoteUrl(gitConfig);
if (!remoteUrl) {
output.error(
`No remote origin URL found in your Git config. Make sure you've configured a remote repo in your local Git config. Run ${chalk.cyan(
'`git remote --help`'
)} for more details.`
);
return 1;
}
output.log(`Identified Git remote "origin": ${link(remoteUrl)}`);
const parsedUrl = parseRepoUrl(remoteUrl);
if (!parsedUrl) {
output.error(
`Failed to parse Git repo data from the following remote URL in your Git config: ${link(
remoteUrl
)}`
);
return 1;
}
const { provider, org: gitOrg, repo } = parsedUrl;
const repoPath = `${gitOrg}/${repo}`;
let connectedRepoPath;
if (!gitProviderLink) {
const connect = await connectGitProvider(
client,
org,
project.id,
provider,
repoPath
);
if (typeof connect === 'number') {
return connect;
}
} else {
const connectedProvider = gitProviderLink.type;
const connectedOrg = gitProviderLink.org;
const connectedRepo = gitProviderLink.repo;
connectedRepoPath = `${connectedOrg}/${connectedRepo}`;
const isSameRepo =
connectedProvider === provider &&
connectedOrg === gitOrg &&
connectedRepo === repo;
if (isSameRepo) {
output.log(
`${chalk.cyan(connectedRepoPath)} is already connected to your project.`
);
return 1;
}
const shouldReplaceRepo = await confirmRepoConnect(
client,
output,
confirm,
connectedRepoPath
);
if (!shouldReplaceRepo) {
return 0;
}
await disconnectGitProvider(client, org, project.id);
const connect = await connectGitProvider(
client,
org,
project.id,
provider,
repoPath
);
if (typeof connect === 'number') {
return connect;
}
}
output.log(
`Connected ${formatProvider(provider)} repository ${chalk.cyan(repoPath)}!`
);
return 0;
}
async function confirmRepoConnect(
client: Client,
output: Output,
yes: boolean,
connectedRepoPath: string
) {
let shouldReplaceProject = yes;
if (!shouldReplaceProject) {
shouldReplaceProject = await confirm(
client,
`Looks like you already have a repository connected: ${chalk.cyan(
connectedRepoPath
)}. Do you want to replace it?`,
true
);
if (!shouldReplaceProject) {
output.log(`Aborted. Repo not connected.`);
}
}
return shouldReplaceProject;
}

View File

@@ -0,0 +1,58 @@
import chalk from 'chalk';
import { Org, Project } from '../../types';
import Client from '../../util/client';
import confirm from '../../util/input/confirm';
import { getCommandName } from '../../util/pkg-name';
import { disconnectGitProvider } from '../../util/projects/connect-git-provider';
export default async function disconnect(
client: Client,
args: string[],
project: Project | undefined,
org: Org | undefined
) {
const { output } = client;
if (args.length !== 0) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('project disconnect')}`
)}`
);
return 2;
}
if (!project || !org) {
output.error('An unexpected error occurred.');
return 1;
}
if (project.link) {
const { org: linkOrg, repo } = project.link;
output.print(
`Your Vercel project will no longer create deployments when you push to this repository.\n`
);
const confirmDisconnect = await confirm(
client,
`Are you sure you want to disconnect ${chalk.cyan(
`${linkOrg}/${repo}`
)} from your project?`,
false
);
if (confirmDisconnect) {
await disconnectGitProvider(client, org, project.id);
output.log(`Disconnected ${chalk.cyan(`${linkOrg}/${repo}`)}.`);
} else {
output.log('Aborted.');
}
} else {
output.error(
`No Git repository connected. Run ${getCommandName(
'project connect'
)} to connect one.`
);
return 1;
}
return 0;
}

View File

@@ -0,0 +1,94 @@
import chalk from 'chalk';
import Client from '../../util/client';
import { ensureLink } from '../../util/ensure-link';
import getArgs from '../../util/get-args';
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
import handleError from '../../util/handle-error';
import logo from '../../util/output/logo';
import { getPkgName } from '../../util/pkg-name';
import validatePaths from '../../util/validate-paths';
import connect from './connect';
import disconnect from './disconnect';
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} git`)} <command>
${chalk.dim('Commands:')}
connect Connect your Git config "origin" remote as a Git provider to your project
disconnect Disconnect the Git provider repository from your project
${chalk.dim('Options:')}
-h, --help Output usage information
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
${chalk.dim('Examples:')}
${chalk.gray('')} Connect a Git provider repository
${chalk.cyan(`$ ${getPkgName()} git connect`)}
${chalk.gray('')} Disconnect the Git provider repository
${chalk.cyan(`$ ${getPkgName()} git disconnect`)}
`);
};
const COMMAND_CONFIG = {
connect: ['connect'],
disconnect: ['disconnect'],
};
export default async function main(client: Client) {
let argv: any;
let subcommand: string | string[];
try {
argv = getArgs(client.argv.slice(2), {
'--confirm': Boolean,
});
} catch (error) {
handleError(error);
return 1;
}
if (argv['--help']) {
help();
return 2;
}
argv._ = argv._.slice(1);
subcommand = argv._[0];
const args = argv._.slice(1);
const confirm = Boolean(argv['--confirm']);
const { output } = client;
let paths = [process.cwd()];
const pathValidation = await validatePaths(client, paths);
if (!pathValidation.valid) {
return pathValidation.exitCode;
}
const { path } = pathValidation;
const linkedProject = await ensureLink('git', client, path, confirm);
if (typeof linkedProject === 'number') {
return linkedProject;
}
const { org, project } = linkedProject;
switch (subcommand) {
case 'connect':
return await connect(client, argv, args, project, org);
case 'disconnect':
return await disconnect(client, args, project, org);
default:
output.error(getInvalidSubcommand(COMMAND_CONFIG));
help();
return 2;
}
}

View File

@@ -14,6 +14,7 @@ export default new Map([
['domain', 'domains'], ['domain', 'domains'],
['domains', 'domains'], ['domains', 'domains'],
['env', 'env'], ['env', 'env'],
['git', 'git'],
['help', 'help'], ['help', 'help'],
['init', 'init'], ['init', 'init'],
['inspect', 'inspect'], ['inspect', 'inspect'],
@@ -25,8 +26,8 @@ export default new Map([
['logout', 'logout'], ['logout', 'logout'],
['logs', 'logs'], ['logs', 'logs'],
['ls', 'list'], ['ls', 'list'],
['project', 'projects'], ['project', 'project'],
['projects', 'projects'], ['projects', 'project'],
['pull', 'pull'], ['pull', 'pull'],
['remove', 'remove'], ['remove', 'remove'],
['rm', 'remove'], ['rm', 'remove'],

View File

@@ -0,0 +1,55 @@
import chalk from 'chalk';
import ms from 'ms';
import Client from '../../util/client';
import { getCommandName } from '../../util/pkg-name';
export default async function add(
client: Client,
args: string[],
contextName: string
) {
const { output } = client;
if (args.length !== 1) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('project add <name>')}`
)}`
);
if (args.length > 1) {
const example = chalk.cyan(
`${getCommandName(`project add "${args.join(' ')}"`)}`
);
output.log(
`If your project name has spaces, make sure to wrap it in quotes. Example: \n ${example} `
);
}
return 1;
}
const start = Date.now();
const [name] = args;
try {
await client.fetch('/projects', {
method: 'POST',
body: { name },
});
} catch (error) {
if (error.status === 409) {
// project already exists, so we can
// show a success message
} else {
throw error;
}
}
const elapsed = ms(Date.now() - start);
output.log(
`${chalk.cyan('Success!')} Project ${chalk.bold(
name.toLowerCase()
)} added (${chalk.bold(contextName)}) ${chalk.gray(`[${elapsed}]`)}`
);
return;
}

View File

@@ -0,0 +1,102 @@
import chalk from 'chalk';
import Client from '../../util/client';
import getArgs from '../../util/get-args';
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
import getScope from '../../util/get-scope';
import handleError from '../../util/handle-error';
import logo from '../../util/output/logo';
import { getPkgName } from '../../util/pkg-name';
import add from './add';
import list from './list';
import rm from './rm';
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} project`)} [options] <command>
${chalk.dim('Commands:')}
ls Show all projects in the selected team/user
add [name] Add a new project
rm [name] Remove a project
${chalk.dim('Options:')}
-h, --help Output usage information
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-S, --scope Set a custom scope
-N, --next Show next page of results
${chalk.dim('Examples:')}
${chalk.gray('')} Add a new project
${chalk.cyan(`$ ${getPkgName()} project add my-project`)}
${chalk.gray('')} Paginate projects, where ${chalk.dim(
'`1584722256178`'
)} is the time in milliseconds since the UNIX epoch.
${chalk.cyan(`$ ${getPkgName()} project ls --next 1584722256178`)}
`);
};
const COMMAND_CONFIG = {
ls: ['ls', 'list'],
add: ['add'],
rm: ['rm', 'remove'],
};
export default async function main(client: Client) {
let argv: any;
let subcommand: string | string[];
try {
argv = getArgs(client.argv.slice(2), {
'--next': Number,
'-N': '--next',
});
} catch (error) {
handleError(error);
return 1;
}
if (argv['--help']) {
help();
return 2;
}
argv._ = argv._.slice(1);
subcommand = argv._[0] || 'list';
const args = argv._.slice(1);
const { output } = client;
let contextName = '';
try {
({ contextName } = await getScope(client));
} catch (error) {
if (error.code === 'NOT_AUTHORIZED' || error.code === 'TEAM_DELETED') {
output.error(error.message);
return 1;
}
throw error;
}
switch (subcommand) {
case 'ls':
case 'list':
return await list(client, argv, args, contextName);
case 'add':
return await add(client, args, contextName);
case 'rm':
case 'remove':
return await rm(client, args);
default:
output.error(getInvalidSubcommand(COMMAND_CONFIG));
help();
return 2;
}
}

View File

@@ -0,0 +1,86 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import Client from '../../util/client';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import strlen from '../../util/strlen';
export default async function list(
client: Client,
argv: any,
args: string[],
contextName: string
) {
const { output } = client;
if (args.length !== 0) {
output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('project ls')}`
)}`
);
return 2;
}
const start = Date.now();
output.spinner(`Fetching projects in ${chalk.bold(contextName)}`);
let projectsUrl = '/v4/projects/?limit=20';
const next = argv['--next'] || false;
if (next) {
projectsUrl += `&until=${next}`;
}
const {
projects: list,
pagination,
}: {
projects: [{ name: string; updatedAt: number }];
pagination: { count: number; next: number };
} = await client.fetch(projectsUrl, {
method: 'GET',
});
output.stopSpinner();
const elapsed = ms(Date.now() - start);
output.log(
`${list.length > 0 ? 'Projects' : 'No projects'} found under ${chalk.bold(
contextName
)} ${chalk.gray(`[${elapsed}]`)}`
);
if (list.length > 0) {
const cur = Date.now();
const header = [['', 'name', 'updated'].map(title => chalk.dim(title))];
const out = table(
header.concat(
list.map(secret => [
'',
chalk.bold(secret.name),
chalk.gray(`${ms(cur - secret.updatedAt)} ago`),
])
),
{
align: ['l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen,
}
);
if (out) {
output.print(`\n${out}\n\n`);
}
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
const nextCmd = `project ls${flags} --next ${pagination.next}`;
output.log(`To display the next page run ${getCommandName(nextCmd)}`);
}
}
return 0;
}

View File

@@ -0,0 +1,63 @@
import chalk from 'chalk';
import ms from 'ms';
import Client from '../../util/client';
import { emoji, prependEmoji } from '../../util/emoji';
import confirm from '../../util/input/confirm';
import { getCommandName } from '../../util/pkg-name';
const e = encodeURIComponent;
export default async function rm(client: Client, args: string[]) {
if (args.length !== 1) {
client.output.error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('project rm <name>')}`
)}`
);
return 1;
}
const name = args[0];
const start = Date.now();
const yes = await readConfirmation(client, name);
if (!yes) {
client.output.log('User abort');
return 0;
}
try {
await client.fetch(`/v2/projects/${e(name)}`, {
method: 'DELETE',
});
} catch (err) {
if (err.status === 404) {
client.output.error('No such project exists');
return 1;
}
}
const elapsed = ms(Date.now() - start);
client.output.log(
`${chalk.cyan('Success!')} Project ${chalk.bold(name)} removed ${chalk.gray(
`[${elapsed}]`
)}`
);
return 0;
}
async function readConfirmation(
client: Client,
projectName: string
): Promise<boolean> {
client.output.print(
prependEmoji(
`The project ${chalk.bold(projectName)} will be removed permanently.\n` +
`It will also delete everything under the project including deployments.\n`,
emoji('warning')
)
);
return await confirm(client, `${chalk.bold.red('Are you sure?')}`, false);
}

View File

@@ -1,302 +0,0 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import strlen from '../util/strlen';
import getArgs from '../util/get-args';
import { handleError, error } from '../util/error';
import exit from '../util/exit';
import logo from '../util/output/logo';
import getScope from '../util/get-scope';
import getCommandFlags from '../util/get-command-flags';
import { getPkgName, getCommandName } from '../util/pkg-name';
import Client from '../util/client';
const e = encodeURIComponent;
const help = () => {
console.log(`
${chalk.bold(`${logo} ${getPkgName()} projects`)} [options] <command>
${chalk.dim('Commands:')}
ls Show all projects in the selected team/user
add [name] Add a new project
rm [name] Remove a project
${chalk.dim('Options:')}
-h, --help Output usage information
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-S, --scope Set a custom scope
-N, --next Show next page of results
${chalk.dim('Examples:')}
${chalk.gray('')} Add a new project
${chalk.cyan(`$ ${getPkgName()} projects add my-project`)}
${chalk.gray('')} Paginate projects, where ${chalk.dim(
'`1584722256178`'
)} is the time in milliseconds since the UNIX epoch.
${chalk.cyan(`$ ${getPkgName()} projects ls --next 1584722256178`)}
`);
};
let argv: any;
let subcommand: string | string[];
const main = async (client: Client) => {
try {
argv = getArgs(client.argv.slice(2), {
'--next': Number,
'-N': '--next',
});
} catch (error) {
handleError(error);
return exit(1);
}
argv._ = argv._.slice(1);
subcommand = argv._[0] || 'list';
if (argv['--help']) {
help();
return exit(2);
}
const { output } = client;
let contextName = null;
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
output.error(err.message);
return 1;
}
throw err;
}
try {
await run({ client, contextName });
} catch (err) {
handleError(err);
exit(1);
}
};
export default async (client: Client) => {
try {
await main(client);
} catch (err) {
handleError(err);
process.exit(1);
}
};
async function run({
client,
contextName,
}: {
client: Client;
contextName: string;
}) {
const { output } = client;
const args = argv._.slice(1);
const start = Date.now();
if (subcommand === 'ls' || subcommand === 'list') {
if (args.length !== 0) {
console.error(
error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('projects ls')}`
)}`
)
);
return exit(2);
}
output.spinner(`Fetching projects in ${chalk.bold(contextName)}`);
let projectsUrl = '/v4/projects/?limit=20';
const next = argv['--next'];
if (next) {
projectsUrl += `&until=${next}`;
}
const {
projects: list,
pagination,
}: {
projects: [{ name: string; updatedAt: number }];
pagination: { count: number; next: number };
} = await client.fetch(projectsUrl, {
method: 'GET',
});
output.stopSpinner();
const elapsed = ms(Date.now() - start);
console.log(
`> ${
list.length > 0 ? 'Projects' : 'No projects'
} found under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`
);
if (list.length > 0) {
const cur = Date.now();
const header = [['', 'name', 'updated'].map(title => chalk.dim(title))];
const out = table(
header.concat(
list.map(secret => [
'',
chalk.bold(secret.name),
chalk.gray(`${ms(cur - secret.updatedAt)} ago`),
])
),
{
align: ['l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen,
}
);
if (out) {
console.log(`\n${out}\n`);
}
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
const nextCmd = `projects ls${flags} --next ${pagination.next}`;
console.log(`To display the next page run ${getCommandName(nextCmd)}`);
}
}
return;
}
if (subcommand === 'rm' || subcommand === 'remove') {
if (args.length !== 1) {
console.error(
error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('project rm <name>')}`
)}`
)
);
return exit(1);
}
const name = args[0];
const yes = await readConfirmation(name);
if (!yes) {
console.error(error('User abort'));
return exit(0);
}
try {
await client.fetch(`/v2/projects/${e(name)}`, {
method: 'DELETE',
});
} catch (err) {
if (err.status === 404) {
console.error(error('No such project exists'));
return exit(1);
}
}
const elapsed = ms(Date.now() - start);
console.log(
`${chalk.cyan('> Success!')} Project ${chalk.bold(
name
)} removed ${chalk.gray(`[${elapsed}]`)}`
);
return;
}
if (subcommand === 'add') {
if (args.length !== 1) {
console.error(
error(
`Invalid number of arguments. Usage: ${chalk.cyan(
`${getCommandName('projects add <name>')}`
)}`
)
);
if (args.length > 1) {
const example = chalk.cyan(
`${getCommandName(`projects add "${args.join(' ')}"`)}`
);
console.log(
`> If your project name has spaces, make sure to wrap it in quotes. Example: \n ${example} `
);
}
return exit(1);
}
const [name] = args;
try {
await client.fetch('/projects', {
method: 'POST',
body: { name },
});
} catch (error) {
if (error.status === 409) {
// project already exists, so we can
// show a success message
} else {
throw error;
}
}
const elapsed = ms(Date.now() - start);
console.log(
`${chalk.cyan('> Success!')} Project ${chalk.bold(
name.toLowerCase()
)} added (${chalk.bold(contextName)}) ${chalk.gray(`[${elapsed}]`)}`
);
return;
}
console.error(error('Please specify a valid subcommand: ls | add | rm'));
help();
exit(2);
}
process.on('uncaughtException', err => {
handleError(err);
exit(1);
});
function readConfirmation(projectName: string) {
return new Promise(resolve => {
process.stdout.write(
`The project: ${chalk.bold(projectName)} will be removed permanently.\n` +
`It will also delete everything under the project including deployments.\n`
);
process.stdout.write(
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
);
process.stdin
.on('data', d => {
process.stdin.pause();
resolve(d.toString().trim().toLowerCase() === 'y');
})
.resume();
});
}

View File

@@ -172,7 +172,8 @@ const main = async () => {
// * a subcommand (as in: `vercel ls`) // * a subcommand (as in: `vercel ls`)
const targetOrSubcommand = argv._[2]; const targetOrSubcommand = argv._[2];
const betaCommands: string[] = ['build']; // Currently no beta commands - add here as needed
const betaCommands: string[] = [];
if (betaCommands.includes(targetOrSubcommand)) { if (betaCommands.includes(targetOrSubcommand)) {
console.log( console.log(
`${chalk.grey( `${chalk.grey(
@@ -631,6 +632,9 @@ const main = async () => {
case 'env': case 'env':
func = require('./commands/env').default; func = require('./commands/env').default;
break; break;
case 'git':
func = require('./commands/git').default;
break;
case 'init': case 'init':
func = require('./commands/init').default; func = require('./commands/init').default;
break; break;
@@ -652,8 +656,8 @@ const main = async () => {
case 'logout': case 'logout':
func = require('./commands/logout').default; func = require('./commands/logout').default;
break; break;
case 'projects': case 'project':
func = require('./commands/projects').default; func = require('./commands/project').default;
break; break;
case 'pull': case 'pull':
func = require('./commands/pull').default; func = require('./commands/pull').default;

View File

@@ -248,12 +248,34 @@ export interface ProjectEnvVariable {
gitBranch?: string; gitBranch?: string;
} }
export interface DeployHook {
createdAt: number;
id: string;
name: string;
ref: string;
url: string;
}
export interface ProjectLinkData {
type: string;
repo: string;
repoId: number;
org?: string;
gitCredentialId: string;
productionBranch?: string | null;
sourceless: boolean;
createdAt: number;
updatedAt: number;
deployHooks?: DeployHook[];
}
export interface Project extends ProjectSettings { export interface Project extends ProjectSettings {
id: string; id: string;
name: string; name: string;
accountId: string; accountId: string;
updatedAt: number; updatedAt: number;
createdAt: number; createdAt: number;
link?: ProjectLinkData;
alias?: ProjectAliasTarget[]; alias?: ProjectAliasTarget[];
latestDeployments?: Partial<Deployment>[]; latestDeployments?: Partial<Deployment>[];
} }

View File

@@ -6,11 +6,9 @@ import { VERCEL_DIR } from '../projects/link';
import readJSONFile from '../read-json-file'; import readJSONFile from '../read-json-file';
export async function initCorepack({ export async function initCorepack({
cwd, repoRootPath,
rootPackageJsonPath,
}: { }: {
cwd: string; repoRootPath: string;
rootPackageJsonPath: string;
}): Promise<string | null> { }): Promise<string | null> {
if (process.env.ENABLE_EXPERIMENTAL_COREPACK !== '1') { if (process.env.ENABLE_EXPERIMENTAL_COREPACK !== '1') {
// Since corepack is experimental, we need to exit early // Since corepack is experimental, we need to exit early
@@ -18,7 +16,7 @@ export async function initCorepack({
return null; return null;
} }
const pkg = await readJSONFile<PackageJson>( const pkg = await readJSONFile<PackageJson>(
join(rootPackageJsonPath, 'package.json') join(repoRootPath, 'package.json')
); );
if (pkg instanceof CantParseJSONFile) { if (pkg instanceof CantParseJSONFile) {
console.warn( console.warn(
@@ -32,16 +30,13 @@ export async function initCorepack({
console.log( console.log(
`Detected ENABLE_EXPERIMENTAL_COREPACK=1 and "${pkg.packageManager}" in package.json` `Detected ENABLE_EXPERIMENTAL_COREPACK=1 and "${pkg.packageManager}" in package.json`
); );
const corepackRootDir = join(cwd, VERCEL_DIR, 'cache', 'corepack'); const corepackRootDir = join(repoRootPath, VERCEL_DIR, 'cache', 'corepack');
const corepackHomeDir = join(corepackRootDir, 'home'); const corepackHomeDir = join(corepackRootDir, 'home');
const corepackShimDir = join(corepackRootDir, 'shim'); const corepackShimDir = join(corepackRootDir, 'shim');
await fs.mkdirp(corepackHomeDir); await fs.mkdirp(corepackHomeDir);
await fs.mkdirp(corepackShimDir); await fs.mkdirp(corepackShimDir);
process.env.COREPACK_HOME = corepackHomeDir; process.env.COREPACK_HOME = corepackHomeDir;
process.env.PATH = `${corepackShimDir}${delimiter}${process.env.PATH}`; process.env.PATH = `${corepackShimDir}${delimiter}${process.env.PATH}`;
process.env.DEBUG = process.env.DEBUG
? `corepack,${process.env.DEBUG}`
: 'corepack';
const pkgManagerName = pkg.packageManager.split('@')[0]; const pkgManagerName = pkg.packageManager.split('@')[0];
// We must explicitly call `corepack enable npm` since `corepack enable` // We must explicitly call `corepack enable npm` since `corepack enable`
// doesn't work with npm. See https://github.com/nodejs/corepack/pull/24 // doesn't work with npm. See https://github.com/nodejs/corepack/pull/24
@@ -72,11 +67,4 @@ export function cleanupCorepack(corepackShimDir: string) {
'' ''
); );
} }
if (process.env.DEBUG) {
if (process.env.DEBUG === 'corepack') {
delete process.env.DEBUG;
} else {
process.env.DEBUG = process.env.DEBUG.replace('corepack,', '');
}
}
} }

View File

@@ -3,19 +3,19 @@ import { join } from 'path';
import ini from 'ini'; import ini from 'ini';
import git from 'git-last-commit'; import git from 'git-last-commit';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { GitMetadata } from '../../types'; import { GitMetadata } from '../types';
import { Output } from '../output'; import { Output } from './output';
export function isDirty(directory: string): Promise<boolean> { export function isDirty(directory: string, output: Output): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise(resolve => {
exec('git status -s', { cwd: directory }, function (err, stdout, stderr) { exec('git status -s', { cwd: directory }, function (err, stdout, stderr) {
if (err) return reject(err); let debugMessage = `Failed to determine if Git repo has been modified:`;
if (stderr) if (err || stderr) {
return reject( if (err) debugMessage += `\n${err}`;
new Error( if (stderr) debugMessage += `\n${stderr.trim()}`;
`Failed to determine if git repo has been modified: ${stderr.trim()}` output.debug(debugMessage);
) return resolve(false);
); }
resolve(stdout.trim().length > 0); resolve(stdout.trim().length > 0);
}); });
}); });
@@ -33,21 +33,31 @@ function getLastCommit(directory: string): Promise<git.Commit> {
}); });
} }
export async function parseGitConfig(configPath: string, output: Output) {
try {
return ini.parse(await fs.readFile(configPath, 'utf-8'));
} catch (error) {
output.debug(`Error while parsing repo data: ${error.message}`);
}
}
export function pluckRemoteUrl(gitConfig: {
[key: string]: any;
}): string | undefined {
// Assuming "origin" is the remote url that the user would want to use
return gitConfig['remote "origin"']?.url;
}
export async function getRemoteUrl( export async function getRemoteUrl(
configPath: string, configPath: string,
output: Output output: Output
): Promise<string | null> { ): Promise<string | null> {
let gitConfig; let gitConfig = await parseGitConfig(configPath, output);
try {
gitConfig = ini.parse(await fs.readFile(configPath, 'utf-8'));
} catch (error) {
output.debug(`Error while parsing repo data: ${error.message}`);
}
if (!gitConfig) { if (!gitConfig) {
return null; return null;
} }
const originUrl: string = gitConfig['remote "origin"']?.url; const originUrl = pluckRemoteUrl(gitConfig);
if (originUrl) { if (originUrl) {
return originUrl; return originUrl;
} }
@@ -64,10 +74,19 @@ export async function createGitMeta(
return; return;
} }
const [commit, dirty] = await Promise.all([ const [commit, dirty] = await Promise.all([
getLastCommit(directory), getLastCommit(directory).catch(err => {
isDirty(directory), output.debug(
`Failed to get last commit. The directory is likely not a Git repo, there are no latest commits, or it is corrupted.\n${err}`
);
return;
}),
isDirty(directory, output),
]); ]);
if (!commit) {
return;
}
return { return {
remoteUrl, remoteUrl,
commitAuthorName: commit.author.name, commitAuthorName: commit.author.name,

View File

@@ -1,4 +1,3 @@
import inquirer from 'inquirer';
import Client from '../client'; import Client from '../client';
import getUser from '../get-user'; import getUser from '../get-user';
import getTeams from '../teams/get-teams'; import getTeams from '../teams/get-teams';
@@ -43,7 +42,7 @@ export default async function selectOrg(
return choices[defaultOrgIndex].value; return choices[defaultOrgIndex].value;
} }
const answers = await inquirer.prompt({ const answers = await client.prompt({
type: 'list', type: 'list',
name: 'org', name: 'org',
message: question, message: question,

View File

@@ -0,0 +1,117 @@
import Client from '../client';
import { stringify } from 'qs';
import { Org } from '../../types';
import chalk from 'chalk';
import link from '../output/link';
export async function disconnectGitProvider(
client: Client,
org: Org,
projectId: string
) {
const fetchUrl = `/v4/projects/${projectId}/link?${stringify({
teamId: org.type === 'team' ? org.id : undefined,
})}`;
return client.fetch(fetchUrl, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
}
export async function connectGitProvider(
client: Client,
org: Org,
projectId: string,
type: string,
repo: string
) {
const fetchUrl = `/v4/projects/${projectId}/link?${stringify({
teamId: org.type === 'team' ? org.id : undefined,
})}`;
try {
return await client.fetch(fetchUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type,
repo,
}),
});
} catch (err) {
if (
err.meta?.action === 'Install GitHub App' ||
err.code === 'repo_not_found'
) {
client.output.error(
`Failed to link ${chalk.cyan(
repo
)}. Make sure there aren't any typos and that you have access to the repository if it's private.`
);
} else if (err.action === 'Add a Login Connection') {
client.output.error(
err.message.replace(repo, chalk.cyan(repo)) +
`\nVisit ${link(err.link)} for more information.`
);
} else {
client.output.error(
`Failed to connect the ${formatProvider(
type
)} repository ${repo}.\n${err}`
);
}
return 1;
}
}
export function formatProvider(type: string): string {
switch (type) {
case 'github':
return 'GitHub';
case 'gitlab':
return 'GitLab';
case 'bitbucket':
return 'Bitbucket';
default:
return type;
}
}
export function parseRepoUrl(originUrl: string): {
provider: string;
org: string;
repo: string;
} | null {
const isSSH = originUrl.startsWith('git@');
// Matches all characters between (// or @) and (.com or .org)
// eslint-disable-next-line prefer-named-capture-group
const provider = /(?<=(\/\/|@)).*(?=(\.com|\.org))/.exec(originUrl);
if (!provider) {
return null;
}
let org;
let repo;
if (isSSH) {
org = originUrl.split(':')[1].split('/')[0];
repo = originUrl.split('/')[1]?.replace('.git', '');
} else {
// Assume https:// or git://
org = originUrl.split('/')[3];
repo = originUrl.split('/')[4]?.replace('.git', '');
}
if (!org || !repo) {
return null;
}
return {
provider: provider[0],
org,
repo,
};
}

View File

@@ -5,6 +5,7 @@ import { join } from 'path';
export type ProjectLinkAndSettings = ProjectLink & { export type ProjectLinkAndSettings = ProjectLink & {
settings: { settings: {
createdAt: Project['createdAt'];
installCommand: Project['installCommand']; installCommand: Project['installCommand'];
buildCommand: Project['buildCommand']; buildCommand: Project['buildCommand'];
devCommand: Project['devCommand']; devCommand: Project['devCommand'];
@@ -28,6 +29,7 @@ export async function writeProjectSettings(
projectId: project.id, projectId: project.id,
orgId: org.id, orgId: org.id,
settings: { settings: {
createdAt: project.createdAt,
framework: project.framework, framework: project.framework,
devCommand: project.devCommand, devCommand: project.devCommand,
installCommand: project.installCommand, installCommand: project.installCommand,

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "bad-remote-url"
}

View File

@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = bababooey
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "existing-connection"
}

View File

@@ -0,0 +1 @@
ref: refs/heads/master

View File

@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/user2/repo2
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "invalid-repo"
}

View File

@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/laksfj/asdgklsadkl
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "new-connection"
}

View File

@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/user/repo
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "no-git-config"
}

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "no-remote-url"
}

View File

@@ -0,0 +1,7 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "new-connection"
}

View File

@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/user/repo
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -0,0 +1,2 @@
!.vercel
.vercel

View File

@@ -0,0 +1 @@
ref: refs/heads/master

View File

@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@@ -0,0 +1,11 @@
[core]
repositoryformatversion = 0
fileMode = false
bare = false
logallrefupdates = true
[user]
name = TechBug2012
email = <>
[remote "origin"]
url = https://github.com/MatthewStanciu/git-test
fetch = +refs/heads/*:refs/remotes/origin/*

View File

@@ -0,0 +1,19 @@
export function pluckIdentifiersFromDeploymentList(output: string): {
project: string | undefined;
org: string | undefined;
} {
const project = output.match(/(?<=Deployments for )(.*)(?= under)/);
const org = output.match(/(?<=under )(.*)(?= \[)/);
return {
project: project?.[0],
org: org?.[0],
};
}
export function parseSpacedTableRow(output: string): string[] {
return output
.trim()
.replace(/ {1} +/g, ',')
.split(',');
}

View File

@@ -0,0 +1,25 @@
import { MockClient } from '../mocks/client';
export function readOutputStream(
client: MockClient,
length: number = 3
): Promise<string> {
return new Promise((resolve, reject) => {
let output: string = '';
let lines = 0;
const timeout = setTimeout(() => {
reject();
}, 3000);
client.stderr.resume();
client.stderr.on('data', chunk => {
output += chunk.toString();
lines++;
if (lines === length) {
clearTimeout(timeout);
resolve(output);
}
});
client.stderr.on('error', reject);
});
}

View File

@@ -1323,12 +1323,7 @@ test('[vc projects] should create a project successfully', async t => {
Math.random().toString(36).split('.')[1] Math.random().toString(36).split('.')[1]
}`; }`;
const vc = execa(binaryPath, [ const vc = execa(binaryPath, ['project', 'add', projectName, ...defaultArgs]);
'projects',
'add',
projectName,
...defaultArgs,
]);
await waitForPrompt(vc, chunk => await waitForPrompt(vc, chunk =>
chunk.includes(`Success! Project ${projectName} added`) chunk.includes(`Success! Project ${projectName} added`)
@@ -1339,7 +1334,7 @@ test('[vc projects] should create a project successfully', async t => {
// creating the same project again should succeed // creating the same project again should succeed
const vc2 = execa(binaryPath, [ const vc2 = execa(binaryPath, [
'projects', 'project',
'add', 'add',
projectName, projectName,
...defaultArgs, ...defaultArgs,
@@ -3517,7 +3512,7 @@ test('`vc --debug project ls` should output the projects listing', async t => {
t.is(exitCode, 0, formatOutput({ stderr, stdout })); t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.true( t.true(
stdout.includes('> Projects found under'), stderr.includes('> Projects found under'),
formatOutput({ stderr, stdout }) formatOutput({ stderr, stdout })
); );
}); });

View File

@@ -25,8 +25,6 @@ type GetMatcherType<TP, TResult> = TP extends PromiseFunction
? (...args: Tail<Parameters<TP>>) => TResult ? (...args: Tail<Parameters<TP>>) => TResult
: TP; : TP;
//type T = GetMatcherType<typeof matchers['toOutput'], void>;
type GetMatchersType<TMatchers, TResult> = { type GetMatchersType<TMatchers, TResult> = {
[P in keyof TMatchers]: GetMatcherType<TMatchers[P], TResult>; [P in keyof TMatchers]: GetMatcherType<TMatchers[P], TResult>;
}; };

View File

@@ -157,6 +157,64 @@ export function useProject(project: Partial<Project> = defaultProject) {
res.json({ envs }); res.json({ envs });
}); });
client.scenario.post(`/v4/projects/${project.id}/link`, (req, res) => {
const { type, repo, org } = req.body;
if (
(type === 'github' || type === 'gitlab' || type === 'bitbucket') &&
(repo === 'user/repo' || repo === 'user2/repo2')
) {
project.link = {
type,
repo,
repoId: 1010,
org,
gitCredentialId: '',
sourceless: true,
createdAt: 1656109539791,
updatedAt: 1656109539791,
};
res.json(project);
} else {
if (type === 'github') {
res.status(400).json({
message: `To link a GitHub repository, you need to install the GitHub integration first. (400)\nInstall GitHub App: https://github.com/apps/vercel`,
meta: {
action: 'Install GitHub App',
link: 'https://github.com/apps/vercel',
repo,
},
});
} else {
res.status(400).json({
code: 'repo_not_found',
message: `The repository "${repo}" couldn't be found in your linked ${formatProvider(
type
)} account.`,
});
}
}
});
client.scenario.delete(`/v4/projects/${project.id}/link`, (_req, res) => {
if (project.link) {
project.link = undefined;
}
res.json(project);
});
client.scenario.get(`/v4/projects`, (req, res) => {
res.json({
projects: [defaultProject],
pagination: null,
});
});
client.scenario.post(`/projects`, (req, res) => {
const { name } = req.body;
if (name === project.name) {
res.json(project);
}
});
client.scenario.delete(`/:version/projects/${project.id}`, (_req, res) => {
res.json({});
});
return { project, envs }; return { project, envs };
} }

View File

@@ -0,0 +1,380 @@
import { join } from 'path';
import fs from 'fs-extra';
import { useUser } from '../../mocks/user';
import { useTeams } from '../../mocks/team';
import { defaultProject, useProject } from '../../mocks/project';
import { client } from '../../mocks/client';
import git from '../../../src/commands/git';
import { Project } from '../../../src/types';
describe('git', () => {
describe('connect', () => {
const originalCwd = process.cwd();
const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/git/connect', name);
it('connects an unlinked project', async () => {
const cwd = fixture('unlinked');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'unlinked',
name: 'unlinked',
});
client.setArgv('projects', 'connect');
const gitPromise = git(client);
await expect(client.stderr).toOutput('Set up');
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
'Which scope should contain your project?'
);
client.stdin.write('\r');
await expect(client.stderr).toOutput('Found project');
client.stdin.write('y\n');
await expect(client.stderr).toOutput(
`Identified Git remote "origin": https://github.com/user/repo.git`
);
const exitCode = await gitPromise;
await expect(client.stderr).toOutput(
'Connected GitHub repository user/repo!'
);
expect(exitCode).toEqual(0);
const project: Project = await client.fetch(`/v8/projects/unlinked`);
expect(project.link).toMatchObject({
type: 'github',
repo: 'user/repo',
repoId: 1010,
gitCredentialId: '',
sourceless: true,
createdAt: 1656109539791,
updatedAt: 1656109539791,
});
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should fail when there is no git config', async () => {
const cwd = fixture('no-git-config');
try {
process.chdir(cwd);
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'no-git-config',
name: 'no-git-config',
});
client.setArgv('projects', 'connect', '--confirm');
const exitCode = await git(client);
expect(exitCode).toEqual(1);
await expect(client.stderr).toOutput(
`Error! No local git repo found. Run \`git clone <url>\` to clone a remote Git repository first.\n`
);
} finally {
process.chdir(originalCwd);
}
});
it('should fail when there is no remote url', async () => {
const cwd = fixture('no-remote-url');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'no-remote-url',
name: 'no-remote-url',
});
client.setArgv('projects', 'connect', '--confirm');
const exitCode = await git(client);
expect(exitCode).toEqual(1);
await expect(client.stderr).toOutput(
`Error! No remote origin URL found in your Git config. Make sure you've configured a remote repo in your local Git config. Run \`git remote --help\` for more details.`
);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should fail when the remote url is bad', async () => {
const cwd = fixture('bad-remote-url');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'bad-remote-url',
name: 'bad-remote-url',
});
client.setArgv('projects', 'connect', '--confirm');
const exitCode = await git(client);
expect(exitCode).toEqual(1);
await expect(client.stderr).toOutput(
`Identified Git remote "origin": bababooey`
);
await expect(client.stderr).toOutput(
`Error! Failed to parse Git repo data from the following remote URL in your Git config: bababooey\n`
);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should connect a repo to a project that is not already connected', async () => {
const cwd = fixture('new-connection');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'new-connection',
name: 'new-connection',
});
client.setArgv('projects', 'connect', '--confirm');
const gitPromise = git(client);
await expect(client.stderr).toOutput(
`Identified Git remote "origin": https://github.com/user/repo`
);
await expect(client.stderr).toOutput(
`> Connected GitHub repository user/repo!\n`
);
const exitCode = await gitPromise;
expect(exitCode).toEqual(0);
const project: Project = await client.fetch(
`/v8/projects/new-connection`
);
expect(project.link).toMatchObject({
type: 'github',
repo: 'user/repo',
repoId: 1010,
gitCredentialId: '',
sourceless: true,
createdAt: 1656109539791,
updatedAt: 1656109539791,
});
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should replace an old connection with a new one', async () => {
const cwd = fixture('existing-connection');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
const project = useProject({
...defaultProject,
id: 'existing-connection',
name: 'existing-connection',
});
project.project.link = {
type: 'github',
repo: 'repo',
org: 'user',
repoId: 1010,
gitCredentialId: '',
sourceless: true,
createdAt: 1656109539791,
updatedAt: 1656109539791,
};
client.setArgv('projects', 'connect', '--confirm');
const gitPromise = git(client);
await expect(client.stderr).toOutput(
`Identified Git remote "origin": https://github.com/user2/repo2`
);
await expect(client.stderr).toOutput(
`> Connected GitHub repository user2/repo2!\n`
);
const exitCode = await gitPromise;
expect(exitCode).toEqual(0);
const newProjectData: Project = await client.fetch(
`/v8/projects/existing-connection`
);
expect(newProjectData.link).toMatchObject({
type: 'github',
repo: 'user2/repo2',
repoId: 1010,
gitCredentialId: '',
sourceless: true,
createdAt: 1656109539791,
updatedAt: 1656109539791,
});
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should exit when an already-connected repo is connected', async () => {
const cwd = fixture('new-connection');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
const project = useProject({
...defaultProject,
id: 'new-connection',
name: 'new-connection',
});
project.project.link = {
type: 'github',
repo: 'repo',
org: 'user',
repoId: 1010,
gitCredentialId: '',
sourceless: true,
createdAt: 1656109539791,
updatedAt: 1656109539791,
};
client.setArgv('projects', 'connect', '--confirm');
const gitPromise = git(client);
await expect(client.stderr).toOutput(
`Identified Git remote "origin": https://github.com/user/repo`
);
await expect(client.stderr).toOutput(
`> user/repo is already connected to your project.\n`
);
const exitCode = await gitPromise;
expect(exitCode).toEqual(1);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should fail when it cannot find the repository', async () => {
const cwd = fixture('invalid-repo');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'invalid-repo',
name: 'invalid-repo',
});
client.setArgv('projects', 'connect', '--confirm');
const gitPromise = git(client);
await expect(client.stderr).toOutput(
`Identified Git remote "origin": https://github.com/laksfj/asdgklsadkl`
);
await expect(client.stderr).toOutput(
`Failed to link laksfj/asdgklsadkl. Make sure there aren't any typos and that you have access to the repository if it's private.`
);
const exitCode = await gitPromise;
expect(exitCode).toEqual(1);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
});
describe('disconnect', () => {
const originalCwd = process.cwd();
const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/git/connect', name);
it('should disconnect a repository', async () => {
const cwd = fixture('new-connection');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
const project = useProject({
...defaultProject,
id: 'new-connection',
name: 'new-connection',
});
project.project.link = {
type: 'github',
repo: 'repo',
org: 'user',
repoId: 1010,
gitCredentialId: '',
sourceless: true,
createdAt: 1656109539791,
updatedAt: 1656109539791,
};
client.setArgv('git', 'disconnect');
const gitPromise = git(client);
await expect(client.stderr).toOutput(
`Are you sure you want to disconnect user/repo from your project?`
);
client.stdin.write('y\n');
await expect(client.stderr).toOutput('Disconnected user/repo.');
const newProjectData: Project = await client.fetch(
`/v8/projects/new-connection`
);
expect(newProjectData.link).toBeUndefined();
const exitCode = await gitPromise;
expect(exitCode).toEqual(0);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
it('should fail if there is no repository to disconnect', async () => {
const cwd = fixture('new-connection');
try {
process.chdir(cwd);
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'new-connection',
name: 'new-connection',
});
client.setArgv('git', 'disconnect');
const gitPromise = git(client);
await expect(client.stderr).toOutput(
'No Git repository connected. Run `vercel project connect` to connect one.'
);
const exitCode = await gitPromise;
expect(exitCode).toEqual(1);
} finally {
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
process.chdir(originalCwd);
}
});
});
});

View File

@@ -1,10 +1,15 @@
import { client, MockClient } from '../../mocks/client'; import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user'; import { useUser } from '../../mocks/user';
import list, { stateString } from '../../../src/commands/list'; import list, { stateString } from '../../../src/commands/list';
import { join } from 'path'; import { join } from 'path';
import { useTeams } from '../../mocks/team'; import { useTeams } from '../../mocks/team';
import { defaultProject, useProject } from '../../mocks/project'; import { defaultProject, useProject } from '../../mocks/project';
import { useDeployment } from '../../mocks/deployment'; import { useDeployment } from '../../mocks/deployment';
import { readOutputStream } from '../../helpers/read-output-stream';
import {
parseSpacedTableRow,
pluckIdentifiersFromDeploymentList,
} from '../../helpers/parse-table';
const fixture = (name: string) => const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/list', name); join(__dirname, '../../fixtures/unit/commands/list', name);
@@ -32,9 +37,9 @@ describe('list', () => {
const output = await readOutputStream(client); const output = await readOutputStream(client);
const { org } = getDataFromIntro(output.split('\n')[0]); const { org } = pluckIdentifiersFromDeploymentList(output.split('\n')[0]);
const header: string[] = parseTable(output.split('\n')[2]); const header: string[] = parseSpacedTableRow(output.split('\n')[2]);
const data: string[] = parseTable(output.split('\n')[3]); const data: string[] = parseSpacedTableRow(output.split('\n')[3]);
data.splice(2, 1); data.splice(2, 1);
expect(org).toEqual(team[0].slug); expect(org).toEqual(team[0].slug);
@@ -74,9 +79,9 @@ describe('list', () => {
const output = await readOutputStream(client); const output = await readOutputStream(client);
const { org } = getDataFromIntro(output.split('\n')[0]); const { org } = pluckIdentifiersFromDeploymentList(output.split('\n')[0]);
const header: string[] = parseTable(output.split('\n')[2]); const header: string[] = parseSpacedTableRow(output.split('\n')[2]);
const data: string[] = parseTable(output.split('\n')[3]); const data: string[] = parseSpacedTableRow(output.split('\n')[3]);
data.splice(2, 1); data.splice(2, 1);
expect(org).toEqual(teamSlug); expect(org).toEqual(teamSlug);
@@ -98,42 +103,3 @@ describe('list', () => {
} }
}); });
}); });
function getDataFromIntro(output: string): {
project: string | undefined;
org: string | undefined;
} {
const project = output.match(/(?<=Deployments for )(.*)(?= under)/);
const org = output.match(/(?<=under )(.*)(?= \[)/);
return {
project: project?.[0],
org: org?.[0],
};
}
function parseTable(output: string): string[] {
return output
.trim()
.replace(/ {3} +/g, ',')
.split(',');
}
function readOutputStream(client: MockClient): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
const timeout = setTimeout(() => {
reject();
}, 3000);
client.stderr.resume();
client.stderr.on('data', chunk => {
chunks.push(chunk);
if (chunks.length === 3) {
clearTimeout(timeout);
resolve(chunks.toString().replace(/,/g, ''));
}
});
client.stderr.on('error', reject);
});
}

View File

@@ -0,0 +1,97 @@
import projects from '../../../src/commands/project';
import { useUser } from '../../mocks/user';
import { useTeams } from '../../mocks/team';
import { defaultProject, useProject } from '../../mocks/project';
import { client } from '../../mocks/client';
import { Project } from '../../../src/types';
import { readOutputStream } from '../../helpers/read-output-stream';
import {
pluckIdentifiersFromDeploymentList,
parseSpacedTableRow,
} from '../../helpers/parse-table';
describe('project', () => {
describe('list', () => {
it('should list deployments under a user', async () => {
const user = useUser();
const project = useProject({
...defaultProject,
});
client.setArgv('project', 'ls');
await projects(client);
const output = await readOutputStream(client, 2);
const { org } = pluckIdentifiersFromDeploymentList(output.split('\n')[0]);
const header: string[] = parseSpacedTableRow(output.split('\n')[2]);
const data: string[] = parseSpacedTableRow(output.split('\n')[3]);
data.pop();
expect(org).toEqual(user.username);
expect(header).toEqual(['name', 'updated']);
expect(data).toEqual([project.project.name]);
});
it('should list deployments for a team', async () => {
useUser();
const team = useTeams('team_dummy');
const project = useProject({
...defaultProject,
});
client.config.currentTeam = team[0].id;
client.setArgv('project', 'ls');
await projects(client);
const output = await readOutputStream(client, 2);
const { org } = pluckIdentifiersFromDeploymentList(output.split('\n')[0]);
const header: string[] = parseSpacedTableRow(output.split('\n')[2]);
const data: string[] = parseSpacedTableRow(output.split('\n')[3]);
data.pop();
expect(org).toEqual(team[0].slug);
expect(header).toEqual(['name', 'updated']);
expect(data).toEqual([project.project.name]);
});
});
describe('add', () => {
it('should add a project', async () => {
const user = useUser();
useProject({
...defaultProject,
id: 'test-project',
name: 'test-project',
});
client.setArgv('project', 'add', 'test-project');
await projects(client);
const project: Project = await client.fetch(`/v8/projects/test-project`);
expect(project).toBeDefined();
expect(client.stderr).toOutput(
`Success! Project test-project added (${user.username})`
);
});
});
describe('rm', () => {
it('should remove a project', async () => {
useUser();
useProject({
...defaultProject,
id: 'test-project',
name: 'test-project',
});
client.setArgv('project', 'rm', 'test-project');
const projectsPromise = projects(client);
await expect(client.stderr).toOutput(
`The project test-project will be removed permanently.`
);
client.stdin.write('y\n');
const exitCode = await projectsPromise;
expect(exitCode).toEqual(0);
});
});
});

View File

@@ -101,7 +101,9 @@ describe('pull', () => {
Object { Object {
"orgId": "team_dummy", "orgId": "team_dummy",
"projectId": "vercel-pull-next", "projectId": "vercel-pull-next",
"settings": Object {}, "settings": Object {
"createdAt": 1555413045188,
},
} }
`); `);
} finally { } finally {

View File

@@ -1,12 +1,15 @@
import { join } from 'path'; import { join } from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import os from 'os';
import { getWriteableDirectory } from '@vercel/build-utils'; import { getWriteableDirectory } from '@vercel/build-utils';
import { import {
createGitMeta, createGitMeta,
getRemoteUrl, getRemoteUrl,
isDirty, isDirty,
} from '../../../../src/util/deploy/create-git-meta'; } from '../../../../src/util/create-git-meta';
import { client } from '../../../mocks/client'; import { client } from '../../../mocks/client';
import { parseRepoUrl } from '../../../../src/util/projects/connect-git-provider';
import { readOutputStream } from '../../../helpers/read-output-stream';
const fixture = (name: string) => const fixture = (name: string) =>
join(__dirname, '../../../fixtures/unit/create-git-meta', name); join(__dirname, '../../../fixtures/unit/create-git-meta', name);
@@ -26,6 +29,97 @@ describe('getRemoteUrl', () => {
}); });
}); });
describe('parseRepoUrl', () => {
it('should be null when a url does not match the regex', () => {
const parsedUrl = parseRepoUrl('https://examplecom/foo');
expect(parsedUrl).toBeNull();
});
it('should be null when a url does not contain org and repo data', () => {
const parsedUrl = parseRepoUrl('https://github.com/borked');
expect(parsedUrl).toBeNull();
});
it('should parse url with a period in the repo name', () => {
const parsedUrl = parseRepoUrl('https://github.com/vercel/next.js');
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('github');
expect(parsedUrl?.org).toEqual('vercel');
expect(parsedUrl?.repo).toEqual('next.js');
});
it('should parse url that ends with .git', () => {
const parsedUrl = parseRepoUrl('https://github.com/vercel/next.js.git');
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('github');
expect(parsedUrl?.org).toEqual('vercel');
expect(parsedUrl?.repo).toEqual('next.js');
});
it('should parse github https url', () => {
const parsedUrl = parseRepoUrl('https://github.com/vercel/vercel.git');
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('github');
expect(parsedUrl?.org).toEqual('vercel');
expect(parsedUrl?.repo).toEqual('vercel');
});
it('should parse github https url without the .git suffix', () => {
const parsedUrl = parseRepoUrl('https://github.com/vercel/vercel');
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('github');
expect(parsedUrl?.org).toEqual('vercel');
expect(parsedUrl?.repo).toEqual('vercel');
});
it('should parse github git url', () => {
const parsedUrl = parseRepoUrl('git://github.com/vercel/vercel.git');
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('github');
expect(parsedUrl?.org).toEqual('vercel');
expect(parsedUrl?.repo).toEqual('vercel');
});
it('should parse github ssh url', () => {
const parsedUrl = parseRepoUrl('git@github.com:vercel/vercel.git');
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('github');
expect(parsedUrl?.org).toEqual('vercel');
expect(parsedUrl?.repo).toEqual('vercel');
});
it('should parse gitlab https url', () => {
const parsedUrl = parseRepoUrl(
'https://gitlab.com/gitlab-examples/knative-kotlin-app.git'
);
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('gitlab');
expect(parsedUrl?.org).toEqual('gitlab-examples');
expect(parsedUrl?.repo).toEqual('knative-kotlin-app');
});
it('should parse gitlab ssh url', () => {
const parsedUrl = parseRepoUrl(
'git@gitlab.com:gitlab-examples/knative-kotlin-app.git'
);
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('gitlab');
expect(parsedUrl?.org).toEqual('gitlab-examples');
expect(parsedUrl?.repo).toEqual('knative-kotlin-app');
});
it('should parse bitbucket https url', () => {
const parsedUrl = parseRepoUrl(
'https://bitbucket.org/atlassianlabs/maven-project-example.git'
);
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('bitbucket');
expect(parsedUrl?.org).toEqual('atlassianlabs');
expect(parsedUrl?.repo).toEqual('maven-project-example');
});
it('should parse bitbucket ssh url', () => {
const parsedUrl = parseRepoUrl(
'git@bitbucket.org:atlassianlabs/maven-project-example.git'
);
expect(parsedUrl).toBeDefined();
expect(parsedUrl?.provider).toEqual('bitbucket');
expect(parsedUrl?.org).toEqual('atlassianlabs');
expect(parsedUrl?.repo).toEqual('maven-project-example');
});
});
describe('createGitMeta', () => { describe('createGitMeta', () => {
it('is undefined when it does not receive a remote url', async () => { it('is undefined when it does not receive a remote url', async () => {
const directory = fixture('no-origin'); const directory = fixture('no-origin');
@@ -41,7 +135,7 @@ describe('createGitMeta', () => {
const directory = fixture('dirty'); const directory = fixture('dirty');
try { try {
await fs.rename(join(directory, 'git'), join(directory, '.git')); await fs.rename(join(directory, 'git'), join(directory, '.git'));
const dirty = await isDirty(directory); const dirty = await isDirty(directory, client.output);
expect(dirty).toBeTruthy(); expect(dirty).toBeTruthy();
} finally { } finally {
await fs.rename(join(directory, '.git'), join(directory, 'git')); await fs.rename(join(directory, '.git'), join(directory, 'git'));
@@ -51,7 +145,7 @@ describe('createGitMeta', () => {
const directory = fixture('not-dirty'); const directory = fixture('not-dirty');
try { try {
await fs.rename(join(directory, 'git'), join(directory, '.git')); await fs.rename(join(directory, 'git'), join(directory, '.git'));
const dirty = await isDirty(directory); const dirty = await isDirty(directory, client.output);
expect(dirty).toBeFalsy(); expect(dirty).toBeFalsy();
} finally { } finally {
await fs.rename(join(directory, '.git'), join(directory, 'git')); await fs.rename(join(directory, '.git'), join(directory, 'git'));
@@ -125,4 +219,29 @@ describe('createGitMeta', () => {
await fs.rename(join(directory, '.git'), join(directory, 'git')); await fs.rename(join(directory, '.git'), join(directory, 'git'));
} }
}); });
it('fails when `.git` is corrupt', async () => {
const directory = fixture('git-corrupt');
const tmpDir = join(os.tmpdir(), 'git-corrupt');
try {
// Copy the fixture into a temp dir so that we don't pick
// up Git information from the `vercel/vercel` repo itself
await fs.copy(directory, tmpDir);
await fs.rename(join(tmpDir, 'git'), join(tmpDir, '.git'));
client.output.debugEnabled = true;
const data = await createGitMeta(tmpDir, client.output);
const output = await readOutputStream(client, 2);
expect(output).toContain(
`Failed to get last commit. The directory is likely not a Git repo, there are no latest commits, or it is corrupted.`
);
expect(output).toContain(
`Failed to determine if Git repo has been modified:`
);
expect(data).toBeUndefined();
} finally {
await fs.remove(tmpDir);
}
});
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/client", "name": "@vercel/client",
"version": "12.1.0", "version": "12.1.1",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"homepage": "https://vercel.com", "homepage": "https://vercel.com",
@@ -42,7 +42,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"@vercel/routing-utils": "1.13.5", "@vercel/routing-utils": "1.13.5",
"@zeit/fetch": "5.2.0", "@zeit/fetch": "5.2.0",
"async-retry": "1.2.3", "async-retry": "1.2.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/fs-detectors", "name": "@vercel/fs-detectors",
"version": "1.0.1", "version": "2.0.0",
"description": "Vercel filesystem detectors", "description": "Vercel filesystem detectors",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",

View File

@@ -13,8 +13,6 @@ interface Metadata {
hasMiddleware: boolean; hasMiddleware: boolean;
} }
const enableFileSystemApiFrameworks = new Set(['solidstart']);
/** /**
* If the Deployment can be built with the new File System API, * If the Deployment can be built with the new File System API,
* return the new Builder. Otherwise an "Exclusion Condition" * return the new Builder. Otherwise an "Exclusion Condition"
@@ -61,11 +59,7 @@ export async function detectFileSystemAPI({
hasMiddleware, hasMiddleware,
}; };
const isEnabled = const isEnabled = enableFlag || hasMiddleware || hasDotOutput;
enableFlag ||
hasMiddleware ||
hasDotOutput ||
enableFileSystemApiFrameworks.has(framework);
if (!isEnabled) { if (!isEnabled) {
return { metadata, fsApiBuilder: null, reason: 'Flag not enabled.' }; return { metadata, fsApiBuilder: null, reason: 'Flag not enabled.' };
} }

View File

@@ -80,11 +80,13 @@ export async function detectFramework({
fs, fs,
frameworkList, frameworkList,
}: DetectFrameworkOptions): Promise<string | null> { }: DetectFrameworkOptions): Promise<string | null> {
for (const framework of frameworkList) { const result = await Promise.all(
if (await matches(fs, framework)) { frameworkList.map(async frameworkMatch => {
return framework.slug; if (await matches(fs, frameworkMatch)) {
} return frameworkMatch.slug;
} }
return null;
return null; })
);
return result.find(res => res !== null) ?? null;
} }

View File

@@ -252,6 +252,19 @@ describe('DetectorFilesystem', () => {
expect(await detectFramework({ fs, frameworkList })).toBe('nextjs'); expect(await detectFramework({ fs, frameworkList })).toBe('nextjs');
}); });
it('Detect frameworks based on ascending order in framework list', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
next: '9.0.0',
gatsby: '4.18.0',
},
}),
});
expect(await detectFramework({ fs, frameworkList })).toBe('nextjs');
});
it('Detect Nuxt.js', async () => { it('Detect Nuxt.js', async () => {
const fs = new VirtualFilesystem({ const fs = new VirtualFilesystem({
'package.json': JSON.stringify({ 'package.json': JSON.stringify({

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/go", "name": "@vercel/go",
"version": "2.0.5", "version": "2.0.6",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go", "homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
@@ -25,7 +25,7 @@
"@types/fs-extra": "^5.0.5", "@types/fs-extra": "^5.0.5",
"@types/node-fetch": "^2.3.0", "@types/node-fetch": "^2.3.0",
"@types/tar": "^4.0.0", "@types/tar": "^4.0.0",
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"async-retry": "1.3.1", "async-retry": "1.3.1",
"execa": "^1.0.0", "execa": "^1.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/hydrogen", "name": "@vercel/hydrogen",
"version": "0.0.2", "version": "0.0.3",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"homepage": "https://vercel.com/docs", "homepage": "https://vercel.com/docs",
@@ -22,7 +22,7 @@
"devDependencies": { "devDependencies": {
"@types/jest": "27.5.1", "@types/jest": "27.5.1",
"@types/node": "*", "@types/node": "*",
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"typescript": "4.6.4" "typescript": "4.6.4"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/next", "name": "@vercel/next",
"version": "3.1.4", "version": "3.1.5",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js", "homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
@@ -45,7 +45,7 @@
"@types/semver": "6.0.0", "@types/semver": "6.0.0",
"@types/text-table": "0.2.1", "@types/text-table": "0.2.1",
"@types/webpack-sources": "3.2.0", "@types/webpack-sources": "3.2.0",
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"@vercel/nft": "0.20.1", "@vercel/nft": "0.20.1",
"@vercel/routing-utils": "1.13.5", "@vercel/routing-utils": "1.13.5",
"async-sema": "3.0.1", "async-sema": "3.0.1",

View File

@@ -399,6 +399,7 @@ export const build: BuildV2 = async ({
const env: typeof process.env = { ...spawnOpts.env }; const env: typeof process.env = { ...spawnOpts.env };
const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128; const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128;
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`; env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
env.NEXT_EDGE_RUNTIME_PROVIDER = 'vercel';
if (target) { if (target) {
// Since version v10.0.8-canary.15 of Next.js the NEXT_PRIVATE_TARGET env // Since version v10.0.8-canary.15 of Next.js the NEXT_PRIVATE_TARGET env

View File

@@ -2148,6 +2148,7 @@ interface EdgeFunctionInfo {
page: string; page: string;
regexp: string; regexp: string;
wasm?: { filePath: string; name: string }[]; wasm?: { filePath: string; name: string }[];
assets?: { filePath: string; name: string }[];
} }
export async function getMiddlewareBundle({ export async function getMiddlewareBundle({
@@ -2234,6 +2235,23 @@ export async function getMiddlewareBundle({
{} {}
); );
const assetFiles = (edgeFunction.assets ?? []).reduce(
(acc: Files, { filePath, name }) => {
const fullFilePath = path.join(
entryPath,
outputDirectory,
filePath
);
acc[`assets/${name}`] = new FileFsRef({
mode: 0o644,
contentType: 'application/octet-stream',
fsPath: fullFilePath,
});
return acc;
},
{}
);
return new EdgeFunction({ return new EdgeFunction({
deploymentTarget: 'v8-worker', deploymentTarget: 'v8-worker',
name: edgeFunction.name, name: edgeFunction.name,
@@ -2251,9 +2269,16 @@ export async function getMiddlewareBundle({
}), }),
}), }),
...wasmFiles, ...wasmFiles,
...assetFiles,
}, },
entrypoint: 'index.js', entrypoint: 'index.js',
envVarsInUse: edgeFunction.env, envVarsInUse: edgeFunction.env,
assets: (edgeFunction.assets ?? []).map(({ name }) => {
return {
name,
path: `assets/${name}`,
};
}),
}); });
})(), })(),
routeSrc: getRouteSrc(edgeFunction, routesManifest), routeSrc: getRouteSrc(edgeFunction, routesManifest),

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/node", "name": "@vercel/node",
"version": "2.4.1", "version": "2.4.3",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js", "homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -31,7 +31,7 @@
}, },
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"@vercel/node-bridge": "3.0.0", "@vercel/node-bridge": "3.0.0",
"@vercel/static-config": "2.0.1", "@vercel/static-config": "2.0.1",
"edge-runtime": "1.0.1", "edge-runtime": "1.0.1",

View File

@@ -193,7 +193,7 @@ async function compileUserCode(entrypoint: string) {
let edgeHandler = module.exports.default; let edgeHandler = module.exports.default;
if (!edgeHandler) { if (!edgeHandler) {
throw new Error('No default export was found. Add a default export to handle requests.'); throw new Error('No default export was found. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware');
} }
let response = await edgeHandler(event.request, event); let response = await edgeHandler(event.request, event);
@@ -305,7 +305,7 @@ function parseRuntime(
throw new Error( throw new Error(
`Invalid function runtime "${runtime}" for "${entrypoint}". Valid runtimes are: ${JSON.stringify( `Invalid function runtime "${runtime}" for "${entrypoint}". Valid runtimes are: ${JSON.stringify(
validRuntimes validRuntimes
)}` )}. Learn more: https://vercel.link/creating-edge-functions`
); );
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/python", "name": "@vercel/python",
"version": "3.0.5", "version": "3.0.6",
"main": "./dist/index.js", "main": "./dist/index.js",
"license": "MIT", "license": "MIT",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python", "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -23,7 +23,7 @@
"devDependencies": { "devDependencies": {
"@types/execa": "^0.9.0", "@types/execa": "^0.9.0",
"@types/jest": "27.4.1", "@types/jest": "27.4.1",
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"execa": "^1.0.0", "execa": "^1.0.0",
"typescript": "4.3.4" "typescript": "4.3.4"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/redwood", "name": "@vercel/redwood",
"version": "1.0.6", "version": "1.0.7",
"main": "./dist/index.js", "main": "./dist/index.js",
"license": "MIT", "license": "MIT",
"homepage": "https://vercel.com/docs", "homepage": "https://vercel.com/docs",
@@ -28,6 +28,6 @@
"@types/aws-lambda": "8.10.19", "@types/aws-lambda": "8.10.19",
"@types/node": "*", "@types/node": "*",
"@types/semver": "6.0.0", "@types/semver": "6.0.0",
"@vercel/build-utils": "5.0.1" "@vercel/build-utils": "5.0.2"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/remix", "name": "@vercel/remix",
"version": "1.0.6", "version": "1.0.8",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
"homepage": "https://vercel.com/docs", "homepage": "https://vercel.com/docs",
@@ -26,7 +26,7 @@
"devDependencies": { "devDependencies": {
"@types/jest": "27.5.1", "@types/jest": "27.5.1",
"@types/node": "*", "@types/node": "*",
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"typescript": "4.6.4" "typescript": "4.6.4"
} }
} }

View File

@@ -1,5 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { dirname, join } from 'path'; import { dirname, join, relative } from 'path';
import { import {
debug, debug,
download, download,
@@ -187,6 +187,15 @@ export const build: BuildV2 = async ({
// Explicit directory path the server output will be // Explicit directory path the server output will be
serverBuildPath = join(remixConfig.serverBuildDirectory, 'index.js'); serverBuildPath = join(remixConfig.serverBuildDirectory, 'index.js');
} }
// Also check for whether were in a monorepo.
// If we are, prepend the app root directory from config onto the build path.
// e.g. `/apps/my-remix-app/api/index.js`
const isMonorepo = repoRootPath && repoRootPath !== workPath;
if (isMonorepo) {
const rootDirectory = relative(repoRootPath, workPath);
serverBuildPath = join(rootDirectory, serverBuildPath);
}
} catch (err: any) { } catch (err: any) {
// Ignore error if `remix.config.js` does not exist // Ignore error if `remix.config.js` does not exist
if (err.code !== 'MODULE_NOT_FOUND') throw err; if (err.code !== 'MODULE_NOT_FOUND') throw err;
@@ -196,6 +205,7 @@ export const build: BuildV2 = async ({
glob('**', join(entrypointFsDirname, 'public')), glob('**', join(entrypointFsDirname, 'public')),
createRenderFunction( createRenderFunction(
entrypointFsDirname, entrypointFsDirname,
repoRootPath,
serverBuildPath, serverBuildPath,
needsHandler, needsHandler,
nodeVersion nodeVersion
@@ -230,6 +240,7 @@ function hasScript(scriptName: string, pkg: PackageJson | null) {
} }
async function createRenderFunction( async function createRenderFunction(
entrypointDir: string,
rootDir: string, rootDir: string,
serverBuildPath: string, serverBuildPath: string,
needsHandler: boolean, needsHandler: boolean,
@@ -250,6 +261,7 @@ async function createRenderFunction(
// Trace the handler with `@vercel/nft` // Trace the handler with `@vercel/nft`
const trace = await nodeFileTrace([handlerPath], { const trace = await nodeFileTrace([handlerPath], {
base: rootDir, base: rootDir,
processCwd: entrypointDir,
}); });
for (const warning of trace.warnings) { for (const warning of trace.warnings) {

View File

@@ -1,7 +1,7 @@
{ {
"name": "@vercel/ruby", "name": "@vercel/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>", "author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "1.3.13", "version": "1.3.14",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby", "homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",
@@ -23,7 +23,7 @@
"devDependencies": { "devDependencies": {
"@types/fs-extra": "8.0.0", "@types/fs-extra": "8.0.0",
"@types/semver": "6.0.0", "@types/semver": "6.0.0",
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"execa": "2.0.4", "execa": "2.0.4",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@vercel/static-build", "name": "@vercel/static-build",
"version": "1.0.5", "version": "1.0.6",
"license": "MIT", "license": "MIT",
"main": "./dist/index", "main": "./dist/index",
"homepage": "https://vercel.com/docs/build-step", "homepage": "https://vercel.com/docs/build-step",
@@ -37,7 +37,7 @@
"@types/ms": "0.7.31", "@types/ms": "0.7.31",
"@types/node-fetch": "2.5.4", "@types/node-fetch": "2.5.4",
"@types/promise-timeout": "1.3.0", "@types/promise-timeout": "1.3.0",
"@vercel/build-utils": "5.0.1", "@vercel/build-utils": "5.0.2",
"@vercel/frameworks": "1.1.0", "@vercel/frameworks": "1.1.0",
"@vercel/ncc": "0.24.0", "@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "1.13.5", "@vercel/routing-utils": "1.13.5",

View File

@@ -20,7 +20,7 @@
], ],
"build": { "build": {
"env": { "env": {
"ENABLE_FILE_SYSTEM_API": "1" "ENABLE_VC_BUILD": "1"
} }
}, },
"github": { "github": {