Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cli] Add support for vc build command with repo link #10075

Merged
merged 4 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/red-wolves-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'vercel': patch
---

Add support for `vc build` command with repo link
5 changes: 5 additions & 0 deletions internals/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,11 @@ export interface ProjectLink {
* to the root directory of the repository.
*/
repoRoot?: string;
/**
* When linked as a repository, contains the relative path
* to the selected project root directory.
*/
projectRootDirectory?: string;
}

export interface PaginationOptions {
Expand Down
37 changes: 26 additions & 11 deletions packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import dotenv from 'dotenv';
import semver from 'semver';
import minimatch from 'minimatch';
import { join, normalize, relative, resolve, sep } from 'path';
import frameworks from '@vercel/frameworks';
import {
getDiscontinuedNodeVersions,
normalizePath,
Expand All @@ -22,9 +25,9 @@ import {
import {
detectBuilders,
detectFrameworkRecord,
detectFrameworkVersion,
LocalFileSystemDetector,
} from '@vercel/fs-detectors';
import minimatch from 'minimatch';
import {
appendRoutesToPhase,
getTransformedRoutes,
Expand All @@ -49,7 +52,7 @@ import {
ProjectLinkAndSettings,
readProjectSettings,
} from '../util/projects/project-settings';
import { VERCEL_DIR } from '../util/projects/link';
import { getProjectLink, VERCEL_DIR } from '../util/projects/link';
import confirm from '../util/input/confirm';
import { emoji, prependEmoji } from '../util/emoji';
import stamp from '../util/output/stamp';
Expand All @@ -63,11 +66,7 @@ import { initCorepack, cleanupCorepack } from '../util/build/corepack';
import { sortBuilders } from '../util/build/sort-builders';
import { toEnumerableError } from '../util/error';
import { validateConfig } from '../util/validate-config';

import { setMonorepoDefaultSettings } from '../util/build/monorepo';
import frameworks from '@vercel/frameworks';
import { detectFrameworkVersion } from '@vercel/fs-detectors';
import semver from 'semver';

type BuildResult = BuildResultV2 | BuildResultV3;

Expand Down Expand Up @@ -134,7 +133,8 @@ const help = () => {
};

export default async function main(client: Client): Promise<number> {
const { cwd, output } = client;
let { cwd } = client;
const { output } = client;

// Ensure that `vc build` is not being invoked recursively
if (process.env.__VERCEL_BUILD_RUNNING) {
Expand Down Expand Up @@ -177,10 +177,18 @@ export default async function main(client: Client): Promise<number> {
return 1;
}

// If repo linked, update `cwd` to the repo root
const link = await getProjectLink(client, cwd);
const projectRootDirectory = link?.projectRootDirectory ?? '';
if (link?.repoRoot) {
cwd = client.cwd = link.repoRoot;
}

// TODO: read project settings from the API, fall back to local `project.json` if that fails

// Read project settings, and pull them from Vercel if necessary
let project = await readProjectSettings(join(cwd, VERCEL_DIR));
const vercelDir = join(cwd, projectRootDirectory, VERCEL_DIR);
let project = await readProjectSettings(vercelDir);
const isTTY = process.stdin.isTTY;
while (!project?.settings) {
let confirmed = yes;
Expand All @@ -207,6 +215,7 @@ export default async function main(client: Client): Promise<number> {
return 0;
}
const { argv: originalArgv } = client;
client.cwd = join(cwd, projectRootDirectory);
client.argv = [
...originalArgv.slice(0, 2),
'pull',
Expand All @@ -217,12 +226,13 @@ export default async function main(client: Client): Promise<number> {
if (result !== 0) {
return result;
}
client.cwd = cwd;
client.argv = originalArgv;
project = await readProjectSettings(join(cwd, VERCEL_DIR));
project = await readProjectSettings(vercelDir);
}

// Delete output directory from potential previous build
const defaultOutputDir = join(cwd, OUTPUT_DIR);
const defaultOutputDir = join(cwd, projectRootDirectory, OUTPUT_DIR);
const outputDir = argv['--output']
? resolve(argv['--output'])
: defaultOutputDir;
Expand All @@ -241,7 +251,12 @@ export default async function main(client: Client): Promise<number> {
const envToUnset = new Set<string>(['VERCEL', 'NOW_BUILDER']);

try {
const envPath = join(cwd, VERCEL_DIR, `.env.${target}.local`);
const envPath = join(
cwd,
projectRootDirectory,
VERCEL_DIR,
`.env.${target}.local`
);
// TODO (maybe?): load env vars from the API, fall back to the local file if that fails
const dotenvResult = dotenv.config({
path: envPath,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default async function main(client: Client) {
return argv;
}

let cwd = argv._[1] || process.cwd();
let cwd = argv._[1] || client.cwd;
const autoConfirm = Boolean(argv['--yes']);
const environment = parseEnvironment(argv['--environment'] || undefined);

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/util/link/ensure-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import type { ProjectLinked } from '@vercel-internals/types';
* directory
* @param opts.projectName - The project name to use when linking, otherwise
* the current directory
* @returns {Promise<LinkResult|number>} Returns a numeric exit code when aborted or
* @returns {Promise<ProjectLinked|number>} Returns a numeric exit code when aborted or
* error, otherwise an object containing the org an project
*/
export async function ensureLink(
commandName: string,
client: Client,
cwd: string,
opts: SetupAndLinkOptions
opts: SetupAndLinkOptions = {}
): Promise<ProjectLinked | number> {
let { link } = opts;
if (!link) {
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/util/projects/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function getVercelDirectory(cwd: string): string {
return existingDirs[0] || possibleDirs[0];
}

async function getProjectLink(
export async function getProjectLink(
client: Client,
path: string
): Promise<ProjectLink | null> {
Expand Down Expand Up @@ -108,9 +108,10 @@ async function getProjectLinkFromRepoLink(
}
if (project) {
return {
repoRoot: repoLink.rootPath,
orgId: repoLink.repoConfig.orgId,
projectId: project.id,
repoRoot: repoLink.rootPath,
projectRootDirectory: project.directory,
};
}
return null;
Expand Down
31 changes: 26 additions & 5 deletions packages/cli/src/util/projects/project-settings.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { outputJSON } from 'fs-extra';
import { Org, Project, ProjectLink } from '@vercel-internals/types';
import { getLinkFromDir, VERCEL_DIR, VERCEL_DIR_PROJECT } from './link';
import { join } from 'path';
import { outputJSON, readFile } from 'fs-extra';
import { VercelConfig } from '@vercel/client';
import { VERCEL_DIR, VERCEL_DIR_PROJECT } from './link';
import { PartialProjectSettings } from '../input/edit-project-settings';
import type { Org, Project, ProjectLink } from '@vercel-internals/types';
import { isErrnoException, isError } from '@vercel/error-utils';

export type ProjectLinkAndSettings = Partial<ProjectLink> & {
settings: {
Expand Down Expand Up @@ -61,8 +62,28 @@ export async function writeProjectSettings(
});
}

export async function readProjectSettings(cwd: string) {
return await getLinkFromDir<ProjectLinkAndSettings>(cwd);
export async function readProjectSettings(vercelDir: string) {
try {
return JSON.parse(
await readFile(join(vercelDir, VERCEL_DIR_PROJECT), 'utf8')
);
} catch (err: unknown) {
// `project.json` file does not exists, so project settings have not been pulled
if (
isErrnoException(err) &&
err.code &&
['ENOENT', 'ENOTDIR'].includes(err.code)
) {
return null;
}

// failed to parse JSON, treat the same as if project settings have not been pulled
if (isError(err) && err.name === 'SyntaxError') {
return null;
}

throw err;
}
}

export function pickOverrides(
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/fixtures/unit/monorepo-link/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
!.vercel
dist
!/.vercel
.vercel/output
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "blog",
"scripts": {
"build": "mkdir -p dist && echo blog > dist/index.txt"
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "dashboard",
"scripts": {
"build": "mkdir -p dist && echo dashboard > dist/index.txt"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "marketing",
"scripts": {
"build": "mkdir -p dist && echo marketing > dist/index.txt"
}
}
73 changes: 73 additions & 0 deletions packages/cli/test/unit/commands/build/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1174,4 +1174,77 @@ describe('build', () => {
expect(fs.existsSync(join(output, 'static', 'index.html'))).toBe(true);
expect(fs.existsSync(join(output, 'static', '.env'))).toBe(false);
});

it('should build with `repo.json` link', async () => {
const cwd = fixture('../../monorepo-link');

useUser();
useTeams('team_dummy');

// "blog" app
useProject({
...defaultProject,
id: 'QmScb7GPQt6gsS',
name: 'monorepo-blog',
rootDirectory: 'blog',
outputDirectory: 'dist',
framework: null,
});
let output = join(cwd, 'blog/.vercel/output');
client.cwd = join(cwd, 'blog');
client.setArgv('build', '--yes');
let exitCode = await build(client);
expect(exitCode).toEqual(0);
delete process.env.__VERCEL_BUILD_RUNNING;

let files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.txt']);
expect(
(await fs.readFile(join(output, 'static/index.txt'), 'utf8')).trim()
).toEqual('blog');

// "dashboard" app
useProject({
...defaultProject,
id: 'QmbKpqpiUqbcke',
name: 'monorepo-dashboard',
rootDirectory: 'dashboard',
outputDirectory: 'dist',
framework: null,
});
output = join(cwd, 'dashboard/.vercel/output');
client.cwd = join(cwd, 'dashboard');
client.setArgv('build', '--yes');
exitCode = await build(client);
expect(exitCode).toEqual(0);
delete process.env.__VERCEL_BUILD_RUNNING;

files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.txt']);
expect(
(await fs.readFile(join(output, 'static/index.txt'), 'utf8')).trim()
).toEqual('dashboard');

// "marketing" app
useProject({
...defaultProject,
id: 'QmX6P93ChNDoZP',
name: 'monorepo-marketing',
rootDirectory: 'marketing',
outputDirectory: 'dist',
framework: null,
});
output = join(cwd, 'marketing/.vercel/output');
client.cwd = join(cwd, 'marketing');
client.setArgv('build', '--yes');
exitCode = await build(client);
expect(exitCode).toEqual(0);
delete process.env.__VERCEL_BUILD_RUNNING;

files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.txt']);
expect(
(await fs.readFile(join(output, 'static/index.txt'), 'utf8')).trim()
).toEqual('marketing');
});
});