netlify
Version:
Netlify command line tool
305 lines (302 loc) • 12.1 kB
JavaScript
import assert from 'node:assert';
import inquirer from 'inquirer';
import isEmpty from 'lodash/isEmpty.js';
import { listSites } from '../../lib/api.js';
import { startSpinner } from '../../lib/spinner.js';
import { chalk, logAndThrowError, exit, log, netlifyCommand } from '../../utils/command-helpers.js';
import { ensureNetlifyIgnore } from '../../utils/gitignore.js';
import getRepoData from '../../utils/get-repo-data.js';
import { track } from '../../utils/telemetry/index.js';
const findSiteByRepoUrl = async (api, repoUrl) => {
log();
const spinner = startSpinner({ text: `Looking for projects connected to '${repoUrl}'` });
const sites = await listSites({ api, options: { filter: 'all' } });
if (sites.length === 0) {
spinner.error();
return logAndThrowError(`You don't have any projects yet. Run ${chalk.cyanBright(`${netlifyCommand()} sites:create`)} to create a project.`);
}
const matchingSites = sites.filter(({ build_settings: buildSettings = {} }) => repoUrl === buildSettings.repo_url);
if (matchingSites.length === 0) {
spinner.error();
log(chalk.redBright.bold.underline(`No matching project found`));
log();
log(`No project found with the remote ${repoUrl}.
Double check you are in the correct working directory and a remote origin repo is configured.
Run ${chalk.cyanBright('git remote -v')} to see a list of your git remotes.`);
return exit(1);
}
if (matchingSites.length === 1) {
spinner.success({ text: `Found 1 project connected to ${repoUrl}` });
const [firstSite] = matchingSites;
return firstSite;
}
spinner.warn({ text: `Found ${matchingSites.length} projects connected to ${repoUrl}` });
const { selectedSite } = await inquirer.prompt([
{
type: 'list',
name: 'selectedSite',
message: 'Which project do you want to link?',
choices: matchingSites.map((matchingSite) => ({
name: `${matchingSite.name} - ${matchingSite.ssl_url}`,
value: matchingSite,
})),
},
]);
if (!selectedSite) {
return logAndThrowError('No project selected');
}
return selectedSite;
};
const linkPrompt = async (command, options) => {
const { api, state } = command.netlify;
const SITE_NAME_PROMPT = 'Search by full or partial project name';
const SITE_LIST_PROMPT = 'Choose from a list of your recently updated projects';
const SITE_ID_PROMPT = 'Enter a project ID';
let GIT_REMOTE_PROMPT = 'Use the current git remote origin URL';
let site;
// Get git remote data if exists
const repoData = await getRepoData({ workingDir: command.workingDir, remoteName: options.gitRemoteName });
let linkChoices = [SITE_NAME_PROMPT, SITE_LIST_PROMPT, SITE_ID_PROMPT];
if (!('error' in repoData)) {
// Add git GIT_REMOTE_PROMPT if in a repo
GIT_REMOTE_PROMPT = `Use current git remote origin (${repoData.httpsUrl})`;
linkChoices = [GIT_REMOTE_PROMPT, ...linkChoices];
}
log();
log(`${chalk.cyanBright(`${netlifyCommand()} link`)} will connect this folder to a project on Netlify`);
log();
const { linkType } = await inquirer.prompt([
{
type: 'list',
name: 'linkType',
message: 'How do you want to link this folder to a project?',
choices: linkChoices,
},
]);
let kind;
switch (linkType) {
case GIT_REMOTE_PROMPT: {
// TODO(serhalp): Refactor function to avoid this. We can only be here if `repoData` is not an error.
assert(!('error' in repoData));
kind = 'gitRemote';
site = await findSiteByRepoUrl(api, repoData.httpsUrl);
break;
}
case SITE_NAME_PROMPT: {
kind = 'byName';
const { searchTerm } = await inquirer.prompt([
{
type: 'input',
name: 'searchTerm',
message: 'Enter the project name (or just part of it):',
},
]);
log(`Looking for projects with names containing '${searchTerm}'...`);
log();
let matchingSites = [];
try {
matchingSites = await listSites({
api,
options: { name: searchTerm, filter: 'all' },
});
}
catch (error_) {
if (error_.status === 404) {
return logAndThrowError(`'${searchTerm}' not found`);
}
else {
return logAndThrowError(error_);
}
}
if (!matchingSites || matchingSites.length === 0) {
return logAndThrowError(`No project names found containing '${searchTerm}'.
Run ${chalk.cyanBright(`${netlifyCommand()} link`)} again to try a new search,
or run ${chalk.cyanBright(`npx ${netlifyCommand()} sites:create`)} to create a project.`);
}
if (matchingSites.length > 1) {
log(`Found ${matchingSites.length} matching projects!`);
const { selectedSite } = await inquirer.prompt([
{
type: 'list',
name: 'selectedSite',
message: 'Which project do you want to link?',
paginated: true,
choices: matchingSites.map((matchingSite) => ({ name: matchingSite.name, value: matchingSite })),
},
]);
if (!selectedSite) {
return logAndThrowError('No project selected');
}
site = selectedSite;
}
else {
const [firstSite] = matchingSites;
site = firstSite;
}
break;
}
case SITE_LIST_PROMPT: {
kind = 'fromList';
log(`Fetching recently updated projects...`);
log();
let sites;
try {
sites = await listSites({ api, options: { maxPages: 1, filter: 'all' } });
}
catch (error_) {
return logAndThrowError(error_);
}
if (!sites || sites.length === 0) {
return logAndThrowError(`You don't have any projects yet. Run ${chalk.cyanBright(`${netlifyCommand()} sites:create`)} to create a project.`);
}
const { selectedSite } = await inquirer.prompt([
{
type: 'list',
name: 'selectedSite',
message: 'Which project do you want to link?',
paginated: true,
choices: sites.map((matchingSite) => ({ name: matchingSite.name, value: matchingSite })),
},
]);
if (!selectedSite) {
return logAndThrowError('No project selected');
}
site = selectedSite;
break;
}
case SITE_ID_PROMPT: {
kind = 'bySiteId';
const { siteId } = await inquirer.prompt([
{
type: 'input',
name: 'siteId',
message: 'What is the project ID?',
},
]);
try {
site = await api.getSite({ siteId });
}
catch (error_) {
if (error_.status === 404) {
return logAndThrowError(`Project ID '${siteId}' not found`);
}
else {
return logAndThrowError(error_);
}
}
break;
}
}
if (!site) {
return logAndThrowError(new Error(`No project found`));
}
// Save site ID to config
state.set('siteId', site.id);
await track('sites_linked', {
siteId: site.id,
linkType: 'prompt',
kind,
});
// Log output
log();
log(chalk.greenBright.bold.underline(`Directory Linked`));
log();
log(`Admin url: ${chalk.magentaBright(site.admin_url)}`);
log(`Project url: ${chalk.cyanBright(site.ssl_url || site.url)}`);
log();
log(`You can now run other \`netlify\` cli commands in this directory`);
// FIXME(serhalp): Mismatch between hardcoded `SiteInfo` and generated Netlify API types.
return site;
};
export const link = async (options, command) => {
await command.authenticate();
const { api, repositoryRoot, site: { id: siteId }, siteInfo, state, } = command.netlify;
let initialSiteData;
let newSiteData;
// Add .netlify to .gitignore file
await ensureNetlifyIgnore(repositoryRoot);
// Site id is incorrect
if (siteId && isEmpty(siteInfo)) {
log(`"${siteId}" was not found in your Netlify account.`);
log(`Please double check your project ID and which account you are logged into via \`${netlifyCommand()} status\`.`);
return exit();
}
if (!isEmpty(siteInfo)) {
// If already linked to project, exit and prompt for unlink
initialSiteData = siteInfo;
log(`Project already linked to "${initialSiteData.name}"`);
log(`Admin url: ${initialSiteData.admin_url}`);
log();
log(`To unlink this project, run: ${chalk.cyanBright(`${netlifyCommand()} unlink`)}`);
}
else if (options.id) {
try {
// @ts-expect-error FIXME(serhalp): Mismatch between hardcoded `SiteInfo` and new generated Netlify API types.
newSiteData = await api.getSite({ site_id: options.id });
}
catch (error_) {
if (error_.status === 404) {
return logAndThrowError(new Error(`Project id ${options.id} not found`));
}
else {
return logAndThrowError(error_);
}
}
// Save site ID
state.set('siteId', newSiteData.id);
log(`${chalk.green('✔')} Linked to ${newSiteData.name}`);
await track('sites_linked', {
siteId: newSiteData.id,
linkType: 'manual',
kind: 'byId',
});
}
else if (options.name) {
let results = [];
try {
results = await listSites({
api,
options: {
name: options.name,
filter: 'all',
},
});
}
catch (error_) {
if (error_.status === 404) {
return logAndThrowError(new Error(`${options.name} not found`));
}
else {
return logAndThrowError(error_);
}
}
if (results.length === 0) {
return logAndThrowError(new Error(`No projects found named ${options.name}`));
}
const matchingSiteData = results.find((site) => site.name === options.name) || results[0];
state.set('siteId', matchingSiteData.id);
log(`${chalk.green('✔')} Linked to ${matchingSiteData.name}`);
await track('sites_linked', {
siteId: (matchingSiteData && matchingSiteData.id) || siteId,
linkType: 'manual',
kind: 'byName',
});
}
else if (options.gitRemoteUrl) {
newSiteData = await findSiteByRepoUrl(api, options.gitRemoteUrl);
state.set('siteId', newSiteData.id);
log(`${chalk.green('✔')} Linked to ${newSiteData.name}`);
await track('sites_linked', {
siteId: newSiteData.id,
linkType: 'clone',
kind: 'byRepoUrl',
});
}
else {
newSiteData = await linkPrompt(command, options);
}
// FIXME(serhalp): All the cases above except one (look up by site name) end up *returning*
// the site data. This is probably not intentional and may result in bugs in deploy/init. Investigate.
return initialSiteData || newSiteData;
};
//# sourceMappingURL=link.js.map