mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 04:22:13 +00:00
[cli] Add support for vc build command with repo link (#10075)
When the repo is linked to Vercel with `vc link --repo`, the `vc build` command should be invoked from the project subdirectory (otherwise the project selector is displayed). The output directory is at `<projectRoot>/.vercel/output` instead of at the repo root.
This commit is contained in:
5
.changeset/red-wolves-boil.md
Normal file
5
.changeset/red-wolves-boil.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'vercel': patch
|
||||
---
|
||||
|
||||
Add support for `vc build` command with repo link
|
||||
5
internals/types/index.d.ts
vendored
5
internals/types/index.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
@@ -22,9 +25,9 @@ import {
|
||||
import {
|
||||
detectBuilders,
|
||||
detectFrameworkRecord,
|
||||
detectFrameworkVersion,
|
||||
LocalFileSystemDetector,
|
||||
} from '@vercel/fs-detectors';
|
||||
import minimatch from 'minimatch';
|
||||
import {
|
||||
appendRoutesToPhase,
|
||||
getTransformedRoutes,
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> {
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
@@ -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(
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
!.vercel
|
||||
dist
|
||||
!/.vercel
|
||||
.vercel/output
|
||||
|
||||
6
packages/cli/test/fixtures/unit/monorepo-link/blog/package.json
vendored
Normal file
6
packages/cli/test/fixtures/unit/monorepo-link/blog/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "blog",
|
||||
"scripts": {
|
||||
"build": "mkdir -p dist && echo blog > dist/index.txt"
|
||||
}
|
||||
}
|
||||
6
packages/cli/test/fixtures/unit/monorepo-link/dashboard/package.json
vendored
Normal file
6
packages/cli/test/fixtures/unit/monorepo-link/dashboard/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"scripts": {
|
||||
"build": "mkdir -p dist && echo dashboard > dist/index.txt"
|
||||
}
|
||||
}
|
||||
6
packages/cli/test/fixtures/unit/monorepo-link/marketing/package.json
vendored
Normal file
6
packages/cli/test/fixtures/unit/monorepo-link/marketing/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "marketing",
|
||||
"scripts": {
|
||||
"build": "mkdir -p dist && echo marketing > dist/index.txt"
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user