[cli] Use detectFrameworks() during vc link --repo (#10203)

Allows for multiple frameworks to be detectable within the same root directory. This is basically specifically for Storybook.

<img width="700" alt="Screenshot 2023-07-12 at 6 04 37 PM" src="https://github.com/vercel/vercel/assets/71256/5a240f1e-b000-42ad-b36f-3c151d3cd449">
This commit is contained in:
Nathan Rajlich
2023-07-14 11:11:49 -07:00
committed by GitHub
parent 24e1e3c3be
commit 9969f0ba18
4 changed files with 78 additions and 42 deletions

View File

@@ -0,0 +1,5 @@
---
'vercel': patch
---
Detect multiple frameworks within the same root directory during `vc link --repo`

View File

@@ -15,6 +15,7 @@ import { emoji, prependEmoji } from '../emoji';
import selectOrg from '../input/select-org'; import selectOrg from '../input/select-org';
import { addToGitIgnore } from './add-to-gitignore'; import { addToGitIgnore } from './add-to-gitignore';
import type Client from '../client'; import type Client from '../client';
import type { Framework } from '@vercel/frameworks';
import type { Project } from '@vercel-internals/types'; import type { Project } from '@vercel-internals/types';
import createProject from '../projects/create-project'; import createProject from '../projects/create-project';
import { detectProjects } from '../projects/detect-projects'; import { detectProjects } from '../projects/detect-projects';
@@ -90,7 +91,7 @@ export async function ensureRepoLink(
// they will be ready by the time the projects are listed // they will be ready by the time the projects are listed
const detectedProjectsPromise = detectProjects(rootPath).catch(err => { const detectedProjectsPromise = detectProjects(rootPath).catch(err => {
output.debug(`Failed to detect local projects: ${err}`); output.debug(`Failed to detect local projects: ${err}`);
return new Map<string, string>(); return new Map<string, Framework[]>();
}); });
// Not yet linked, so prompt user to begin linking // Not yet linked, so prompt user to begin linking
@@ -187,17 +188,21 @@ export async function ensureRepoLink(
detectedProjects.delete(project.rootDirectory ?? ''); detectedProjects.delete(project.rootDirectory ?? '');
} }
if (detectedProjects.size > 0) { const detectedProjectsCount = Array.from(detectedProjects.values()).reduce(
(o, f) => o + f.length,
0
);
if (detectedProjectsCount > 0) {
output.log( output.log(
`Detected ${pluralize( `Detected ${pluralize(
'new Project', 'new Project',
detectedProjects.size, detectedProjectsCount,
true true
)} that may be created.` )} that may be created.`
); );
} }
const addSeparators = projects.length > 0 && detectedProjects.size > 0; const addSeparators = projects.length > 0 && detectedProjectsCount > 0;
const { selected } = await client.prompt({ const { selected } = await client.prompt({
type: 'checkbox', type: 'checkbox',
name: 'selected', name: 'selected',
@@ -218,23 +223,30 @@ export async function ensureRepoLink(
...(addSeparators ...(addSeparators
? [new inquirer.Separator('----- New Projects to be created -----')] ? [new inquirer.Separator('----- New Projects to be created -----')]
: []), : []),
...Array.from(detectedProjects.entries()).map( ...Array.from(detectedProjects.entries()).flatMap(
([rootDirectory, framework]) => { ([rootDirectory, frameworks]) =>
frameworks.map((framework, i) => {
const name = slugify( const name = slugify(
[basename(rootPath), basename(rootDirectory)] [
basename(rootPath),
basename(rootDirectory),
i > 0 ? framework.slug : '',
]
.filter(Boolean) .filter(Boolean)
.join('-') .join('-')
); );
return { return {
name: `${org.slug}/${name} (${framework})`, name: `${org.slug}/${name} (${framework.name})`,
value: { value: {
newProject: true, newProject: true,
rootDirectory, rootDirectory,
name, name,
framework, framework,
}, },
// Checked by default when there are no other existing Projects
checked: projects.length === 0,
}; };
} })
), ),
], ],
}); });
@@ -251,7 +263,10 @@ export async function ensureRepoLink(
output.spinner(`Creating new Project: ${orgAndName}`); output.spinner(`Creating new Project: ${orgAndName}`);
delete selection.newProject; delete selection.newProject;
if (!selection.rootDirectory) delete selection.rootDirectory; if (!selection.rootDirectory) delete selection.rootDirectory;
selected[i] = await createProject(client, selection); selected[i] = await createProject(client, {
...selection,
framework: selection.framework.slug,
});
await connectGitProvider( await connectGitProvider(
client, client,
org, org,
@@ -262,7 +277,8 @@ export async function ensureRepoLink(
output.log( output.log(
`Created new Project: ${output.link( `Created new Project: ${output.link(
orgAndName, orgAndName,
`https://vercel.com/${orgAndName}` `https://vercel.com/${orgAndName}`,
{ fallback: false }
)}` )}`
); );
} }
@@ -287,9 +303,11 @@ export async function ensureRepoLink(
output.print( output.print(
prependEmoji( prependEmoji(
`Linked to ${repoUrlLink} under ${chalk.bold( `Linked to ${pluralize(
org.slug 'Project',
)} (created ${VERCEL_DIR}${ selected.length,
true
)} under ${chalk.bold(org.slug)} (created ${VERCEL_DIR}${
isGitIgnoreUpdated ? ' and added it to .gitignore' : '' isGitIgnoreUpdated ? ' and added it to .gitignore' : ''
})`, })`,
emoji('link') emoji('link')

View File

@@ -1,7 +1,7 @@
import { join } from 'path'; import { join } from 'path';
import frameworks from '@vercel/frameworks'; import frameworkList, { Framework } from '@vercel/frameworks';
import { import {
detectFramework, detectFrameworks,
getWorkspacePackagePaths, getWorkspacePackagePaths,
getWorkspaces, getWorkspaces,
LocalFileSystemDetector, LocalFileSystemDetector,
@@ -10,7 +10,7 @@ import {
export async function detectProjects(cwd: string) { export async function detectProjects(cwd: string) {
const fs = new LocalFileSystemDetector(cwd); const fs = new LocalFileSystemDetector(cwd);
const workspaces = await getWorkspaces({ fs }); const workspaces = await getWorkspaces({ fs });
const detectedProjects = new Map<string, string>(); const detectedProjects = new Map<string, Framework[]>();
const packagePaths = ( const packagePaths = (
await Promise.all( await Promise.all(
workspaces.map(workspace => workspaces.map(workspace =>
@@ -26,12 +26,12 @@ export async function detectProjects(cwd: string) {
} }
await Promise.all( await Promise.all(
packagePaths.map(async p => { packagePaths.map(async p => {
const framework = await detectFramework({ const frameworks = await detectFrameworks({
fs: fs.chdir(join('.', p)), fs: fs.chdir(join('.', p)),
frameworkList: frameworks, frameworkList,
}); });
if (!framework) return; if (frameworks.length === 0) return;
detectedProjects.set(p.slice(1), framework); detectedProjects.set(p.slice(1), frameworks);
}) })
); );
return detectedProjects; return detectedProjects;

View File

@@ -1,4 +1,5 @@
import { join } from 'path'; import { join } from 'path';
import type { Framework } from '@vercel/frameworks';
import { detectProjects } from '../../../../src/util/projects/detect-projects'; import { detectProjects } from '../../../../src/util/projects/detect-projects';
const REPO_ROOT = join(__dirname, '../../../../../..'); const REPO_ROOT = join(__dirname, '../../../../../..');
@@ -8,23 +9,35 @@ const FS_DETECTORS_FIXTURES = join(
'packages/fs-detectors/test/fixtures' 'packages/fs-detectors/test/fixtures'
); );
function mapDetected(
detected: Map<string, Framework[]>
): Array<[string, string[]]> {
return [...detected.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([dir, frameworks]) => [dir, frameworks.map(f => f.slug as string)]);
}
describe('detectProjects()', () => { describe('detectProjects()', () => {
it('should match "nextjs" example', async () => { it('should match 1 Project in "nextjs" example', async () => {
const dir = join(EXAMPLES_DIR, 'nextjs'); const dir = join(EXAMPLES_DIR, 'nextjs');
const detected = await detectProjects(dir); const detected = await detectProjects(dir);
expect([...detected.entries()]).toEqual([['', 'nextjs']]); expect(mapDetected(detected)).toEqual([['', ['nextjs']]]);
});
it('should match 2 Projects in "storybook" example', async () => {
const dir = join(EXAMPLES_DIR, 'storybook');
const detected = await detectProjects(dir);
expect(mapDetected(detected)).toEqual([['', ['nextjs', 'storybook']]]);
}); });
it('should match "30-double-nested-workspaces"', async () => { it('should match "30-double-nested-workspaces"', async () => {
const dir = join(FS_DETECTORS_FIXTURES, '30-double-nested-workspaces'); const dir = join(FS_DETECTORS_FIXTURES, '30-double-nested-workspaces');
const detected = await detectProjects(dir); const detected = await detectProjects(dir);
expect( expect(mapDetected(detected)).toEqual([
[...detected.entries()].sort((a, b) => a[0].localeCompare(b[0])) ['packages/backend/c', ['remix']],
).toEqual([ ['packages/backend/d', ['nextjs']],
['packages/backend/c', 'remix'], ['packages/frontend/a', ['hexo']],
['packages/backend/d', 'nextjs'], ['packages/frontend/b', ['ember']],
['packages/frontend/a', 'hexo'],
['packages/frontend/b', 'ember'],
]); ]);
}); });
}); });