netlify
Version:
Netlify command line tool
890 lines • 38.2 kB
JavaScript
import { stat } from 'fs/promises';
import { basename, resolve } from 'path';
import { stdin, stdout } from 'process';
import { runCoreSteps } from '@netlify/build';
import inquirer from 'inquirer';
import isEmpty from 'lodash/isEmpty.js';
import isObject from 'lodash/isObject.js';
import { parseAllHeaders } from '@netlify/headers-parser';
import { parseAllRedirects } from '@netlify/redirect-parser';
import prettyjson from 'prettyjson';
import { cancelDeploy } from '../../lib/api.js';
import { getRunBuildOptions, runBuild, } from '../../lib/build.js';
import { getBootstrapURL } from '../../lib/edge-functions/bootstrap.js';
import { featureFlags as edgeFunctionsFeatureFlags } from '../../lib/edge-functions/consts.js';
import { normalizeFunctionsConfig } from '../../lib/functions/config.js';
import { BACKGROUND_FUNCTIONS_WARNING } from '../../lib/log.js';
import { startSpinner, stopSpinner } from '../../lib/spinner.js';
import { detectFrameworkSettings, getDefaultConfig } from '../../utils/build-info.js';
import { NETLIFY_CYAN_HEX, NETLIFYDEVERR, NETLIFYDEVLOG, chalk, logAndThrowError, exit, getToken, log, logJson, warn, } from '../../utils/command-helpers.js';
import { DEFAULT_DEPLOY_TIMEOUT } from '../../utils/deploy/constants.js';
import { deploySite } from '../../utils/deploy/deploy-site.js';
import { uploadSourceZip } from '../../utils/deploy/upload-source-zip.js';
import { getEnvelopeEnv } from '../../utils/env/index.js';
import { getFunctionsManifestPath, getInternalFunctionsDir } from '../../utils/functions/index.js';
import openBrowser from '../../utils/open-browser.js';
import { link } from '../link/link.js';
import { sitesCreate } from '../sites/sites-create.js';
import boxen from 'boxen';
import terminalLink from 'terminal-link';
import { anyEdgeFunctionsDirectoryExists } from '../../lib/edge-functions/get-directories.js';
const triggerDeploy = async ({ api, options, siteData, siteId, }) => {
try {
const siteBuild = await api.createSiteBuild({ siteId });
if (options.json) {
logJson({
site_id: siteId,
site_name: siteData.name,
deploy_id: `${siteBuild.deploy_id}`,
logs: `https://app.netlify.com/projects/${siteData.name}/deploys/${siteBuild.deploy_id}`,
});
}
else {
log(`${NETLIFYDEVLOG} A new deployment was triggered successfully. Visit https://app.netlify.com/projects/${siteData.name}/deploys/${siteBuild.deploy_id} to see the logs.`);
}
}
catch (error_) {
if (error_.status === 404) {
return logAndThrowError('Project not found. Please rerun "netlify link" and make sure that your project has CI configured.');
}
else {
return logAndThrowError(error_.message);
}
}
};
/** Retrieves the folder containing the static files that need to be deployed */
const getDeployFolder = async ({ command, config, options, site, siteData, }) => {
let deployFolder;
// if the `--dir .` flag is provided we should resolve it to the working directory.
// - in regular sites this is the `process.cwd`
// - in mono repositories this will be the root of the jsWorkspace
if (options.dir) {
deployFolder = command.workspacePackage
? resolve(command.jsWorkspaceRoot || site.root, options.dir)
: resolve(command.workingDir, options.dir);
}
else if (config?.build?.publish) {
deployFolder = resolve(site.root, config.build.publish);
}
else if (siteData?.build_settings?.dir) {
deployFolder = resolve(site.root, siteData.build_settings.dir);
}
if (!deployFolder) {
if (!stdin.isTTY || !stdout.isTTY) {
// non interactive - can't get the value, resolve to the cwd
if (command.workspacePackage) {
return command.jsWorkspaceRoot || site.root;
}
return command.workingDir;
}
log('Please provide a publish directory (e.g. "public" or "dist" or "."):');
// Generate copy-pasteable command with current options
const copyableCommand = generateDeployCommand({ ...options, dir: '<PATH>' }, [], command);
log(`\nTo specify directory non-interactively, use: ${copyableCommand}\n`);
const { promptPath } = await inquirer.prompt([
{
type: 'input',
name: 'promptPath',
message: 'Publish directory',
default: '.',
filter: (input) => resolve(command.workingDir, input),
},
]);
deployFolder = promptPath;
}
return deployFolder;
};
const validateDeployFolder = async (deployFolder) => {
let stats;
try {
stats = await stat(deployFolder);
}
catch (error_) {
if (error_ && typeof error_ === 'object' && 'code' in error_) {
if (error_.code === 'ENOENT') {
return logAndThrowError(`The deploy directory "${deployFolder}" has not been found. Did you forget to run 'netlify build'?`);
}
// Improve the message of permission errors
if (error_.code === 'EACCES') {
return logAndThrowError('Permission error when trying to access deploy folder');
}
}
throw error_;
}
if (!stats.isDirectory()) {
return logAndThrowError('Deploy target must be a path to a directory');
}
return stats;
};
/** get the functions directory */
const getFunctionsFolder = ({ config, options, site, siteData, workingDir, }) => {
let functionsFolder;
// Support "functions" and "Functions"
const funcConfig = config.functionsDirectory;
if (options.functions) {
functionsFolder = resolve(workingDir, options.functions);
}
else if (funcConfig) {
functionsFolder = resolve(site.root, funcConfig);
}
else if (siteData?.build_settings?.functions_dir) {
functionsFolder = resolve(site.root, siteData.build_settings.functions_dir);
}
return functionsFolder;
};
const validateFunctionsFolder = async (functionsFolder) => {
let stats;
if (functionsFolder) {
// we used to hard error if functions folder is specified but doesn't exist
// but this was too strict for onboarding. we can just log a warning.
try {
stats = await stat(functionsFolder);
}
catch (error_) {
if (error_ && typeof error_ === 'object' && 'code' in error_) {
if (error_.code === 'ENOENT') {
log(`Functions folder "${functionsFolder}" specified but it doesn't exist! Will proceed without deploying functions`);
}
// Improve the message of permission errors
if (error_.code === 'EACCES') {
return logAndThrowError('Permission error when trying to access functions folder');
}
}
}
}
if (stats && !stats.isDirectory()) {
return logAndThrowError('Functions folder must be a path to a directory');
}
return stats;
};
const validateFolders = async ({ deployFolder, functionsFolder, }) => {
const deployFolderStat = await validateDeployFolder(deployFolder);
const functionsFolderStat = await validateFunctionsFolder(functionsFolder);
return { deployFolderStat, functionsFolderStat };
};
/**
* @param {object} config
* @param {string} config.deployFolder
* @param {*} config.site
* @returns
*/
// @ts-expect-error TS(7031) FIXME: Binding element 'deployFolder' implicitly has an '... Remove this comment to see the full error message
const getDeployFilesFilter = ({ deployFolder, site }) => {
// site.root === deployFolder can happen when users run `netlify deploy --dir .`
// in that specific case we don't want to publish the repo node_modules
// when site.root !== deployFolder the behaviour matches our buildbot
const skipNodeModules = site.root === deployFolder;
return (filename) => {
if (filename == null) {
return false;
}
if (filename === deployFolder) {
return true;
}
const base = basename(filename);
const skipFile = (skipNodeModules && base === 'node_modules') ||
(base.startsWith('.') && base !== '.well-known') ||
base.startsWith('__MACOSX') ||
base.includes('/.') ||
// headers and redirects are bundled in the config
base === '_redirects' ||
base === '_headers';
return !skipFile;
};
};
const SEC_TO_MILLISEC = 1e3;
// 100 bytes
const SYNC_FILE_LIMIT = 1e2;
// Helper function to generate copy-pasteable deploy command
const generateDeployCommand = (options, availableTeams, command) => {
const parts = ['netlify deploy'];
// Handle site selection/creation first
if (options.createSite) {
const siteName = typeof options.createSite === 'string' ? options.createSite : '<SITE_NAME>';
parts.push(`--create-site ${siteName}`);
if (availableTeams.length > 1) {
parts.push('--team <TEAM_SLUG>');
}
}
else if (options.site) {
parts.push(`--site ${options.site}`);
}
else {
parts.push('--create-site <SITE_NAME>');
if (availableTeams.length > 1) {
parts.push('--team <TEAM_SLUG>');
}
}
if (command?.options) {
for (const option of command.options) {
if (['createSite', 'site', 'team'].includes(option.attributeName())) {
continue;
}
const optionName = option.attributeName();
const value = options[optionName];
if (option.long?.startsWith('--no-')) {
if (value === false) {
parts.push(option.long);
}
continue;
}
if (optionName === 'build') {
continue;
}
if (value && option.long) {
const flag = option.long;
const hasValue = option.required || option.optional;
if (hasValue && typeof value === 'string') {
const quotedValue = optionName === 'message' ? `"${value}"` : value;
parts.push(`${flag} ${quotedValue}`);
}
else if (hasValue && typeof value === 'number') {
parts.push(`${flag} ${value}`);
}
else if (!hasValue && value === true) {
parts.push(flag);
}
}
}
}
return parts.join(' ');
};
// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message
const prepareProductionDeploy = async ({ api, siteData, options, command }) => {
if (isObject(siteData.published_deploy) && siteData.published_deploy.locked) {
log(`\n${NETLIFYDEVERR} Deployments are "locked" for production context of this project\n`);
// Generate copy-pasteable command with current options
const overrideCommand = generateDeployCommand({ ...options, prodIfUnlocked: true, prod: false }, [], command);
log('\nTo override deployment lock (USE WITH CAUTION), use:');
log(` ${overrideCommand}`);
log('\nWarning: Only use --prod-if-unlocked if you are absolutely sure you want to override the deployment lock.\n');
const { unlockChoice } = await inquirer.prompt([
{
type: 'confirm',
name: 'unlockChoice',
message: 'Would you like to "unlock" deployments for production context to proceed?',
default: false,
},
]);
if (!unlockChoice)
exit(0);
await api.unlockDeploy({ deploy_id: siteData.published_deploy.id });
log(`\n${NETLIFYDEVLOG} "Auto publishing" has been enabled for production context\n`);
}
};
const hasErrorMessage = (actual, expected) => {
if (typeof actual === 'string') {
return actual.includes(expected);
}
return false;
};
const reportDeployError = ({ error, failAndExit, }) => {
switch (true) {
case error.name === 'JSONHTTPError': {
const message = error.json?.message ?? '';
if (hasErrorMessage(message, 'Background Functions not allowed by team plan')) {
return failAndExit(`\n${BACKGROUND_FUNCTIONS_WARNING}`);
}
warn(`JSONHTTPError: ${message} ${error.status}`);
warn(`\n${JSON.stringify(error, null, ' ')}\n`);
return failAndExit(error);
}
case error.name === 'TextHTTPError': {
warn(`TextHTTPError: ${error.status}`);
warn(`\n${error}\n`);
return failAndExit(error);
}
case hasErrorMessage(error.message, 'Invalid filename'): {
warn(error.message);
return failAndExit(error);
}
default: {
warn(`\n${JSON.stringify(error, null, ' ')}\n`);
return failAndExit(error);
}
}
};
const deployProgressCb = function () {
const spinnersByType = {};
return (event) => {
switch (event.phase) {
case 'start': {
spinnersByType[event.type] = startSpinner({
text: event.msg,
});
return;
}
case 'progress': {
const spinner = spinnersByType[event.type];
if (spinner) {
spinner.update({ text: event.msg });
}
return;
}
case 'error':
stopSpinner({ error: true, spinner: spinnersByType[event.type], text: event.msg });
delete spinnersByType[event.type];
return;
case 'stop':
default: {
spinnersByType[event.type].success(event.msg);
delete spinnersByType[event.type];
}
}
};
};
const uploadDeployBlobs = async ({ cachedConfig, deployId, options, packagePath, silent, siteId, }) => {
const statusCb = silent ? () => { } : deployProgressCb();
statusCb({
type: 'blobs-uploading',
msg: 'Uploading blobs to deploy store...\n',
phase: 'start',
});
const [token] = await getToken();
const blobsToken = token || undefined;
const { success } = await runCoreSteps(['blobs_upload'], {
...options,
// We log our own progress so we don't want this as well. Plus, this logs much of the same
// information as the build that (likely) came before this as part of the deploy build.
quiet: options.debug ?? true,
// @ts-expect-error(serhalp) -- Untyped in `@netlify/build`
cachedConfig,
packagePath,
deployId,
siteId,
token: blobsToken,
});
if (!success) {
statusCb({
type: 'blobs-uploading',
msg: 'Deploy aborted due to error while uploading blobs to deploy store',
phase: 'error',
});
return logAndThrowError('Error while uploading blobs to deploy store');
}
statusCb({
type: 'blobs-uploading',
msg: 'Finished uploading blobs to deploy store',
phase: 'stop',
});
};
const runDeploy = async ({
// @ts-expect-error TS(7031) FIXME: Binding element 'alias' implicitly has an 'any' ty... Remove this comment to see the full error message
alias,
// @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message
api, command,
// @ts-expect-error TS(7031) FIXME: Binding element 'config' implicitly has an 'any' t... Remove this comment to see the full error message
config,
// @ts-expect-error TS(7031) FIXME: Binding element 'deployFolder' implicitly has an '... Remove this comment to see the full error message
deployFolder,
// @ts-expect-error TS(7031) FIXME: Binding element 'deployTimeout' implicitly has an ... Remove this comment to see the full error message
deployTimeout,
// @ts-expect-error TS(7031) FIXME: Binding element 'deployToProduction' implicitly ha... Remove this comment to see the full error message
deployToProduction,
// @ts-expect-error TS(7031) FIXME: Binding element 'functionsConfig' implicitly has a... Remove this comment to see the full error message
functionsConfig, functionsFolder,
// @ts-expect-error TS(7031) FIXME: Binding element 'options' implicitly has an 'a... Remove this comment to see the full error message
options,
// @ts-expect-error TS(7031) FIXME: Binding element 'packagePath' implicitly has an 'a... Remove this comment to see the full error message
packagePath,
// @ts-expect-error TS(7031) FIXME: Binding element 'silent' implicitly has an 'any' t... Remove this comment to see the full error message
silent,
// @ts-expect-error TS(7031) FIXME: Binding element 'site' implicitly has an 'any' typ... Remove this comment to see the full error message
site,
// @ts-expect-error TS(7031) FIXME: Binding element 'siteData' implicitly has an 'any'... Remove this comment to see the full error message
siteData,
// @ts-expect-error TS(7031) FIXME: Binding element 'siteId' implicitly has an 'any' t... Remove this comment to see the full error message
siteId,
// @ts-expect-error TS(7031) FIXME: Binding element 'skipFunctionsCache' implicitly ha... Remove this comment to see the full error message
skipFunctionsCache,
// @ts-expect-error TS(7031) FIXME: Binding element 'title' implicitly has an 'any' ty... Remove this comment to see the full error message
title, deployId: existingDeployId, }) => {
let results;
let deployId = existingDeployId;
let uploadSourceZipResult;
try {
// We won't have a deploy ID if we run the command with `--no-build`.
// In this case, we must create the deploy.
if (!deployId) {
if (deployToProduction) {
await prepareProductionDeploy({ siteData, api, options, command });
}
const draft = options.draft || (!deployToProduction && !alias);
const createDeployBody = { draft, branch: alias, include_upload_url: options.uploadSourceZip };
const createDeployResponse = await api.createSiteDeploy({ siteId, title, body: createDeployBody });
deployId = createDeployResponse.id;
if (options.uploadSourceZip &&
createDeployResponse.source_zip_upload_url &&
createDeployResponse.source_zip_filename) {
uploadSourceZipResult = await uploadSourceZip({
sourceDir: site.root,
uploadUrl: createDeployResponse.source_zip_upload_url,
filename: createDeployResponse.source_zip_filename,
statusCb: silent ? () => { } : deployProgressCb(),
});
}
}
const internalFunctionsFolder = await getInternalFunctionsDir({ base: site.root, packagePath, ensureExists: true });
await command.netlify.frameworksAPIPaths.functions.ensureExists();
// The order of the directories matter: zip-it-and-ship-it will prioritize
// functions from the rightmost directories. In this case, we want user
// functions to take precedence over internal functions.
const functionDirectories = [
internalFunctionsFolder,
command.netlify.frameworksAPIPaths.functions.path,
functionsFolder,
].filter((folder) => Boolean(folder));
const manifestPath = skipFunctionsCache ? null : await getFunctionsManifestPath({ base: site.root, packagePath });
const redirectsPath = `${deployFolder}/_redirects`;
const headersPath = `${deployFolder}/_headers`;
const { redirects } = await parseAllRedirects({
configRedirects: config.redirects,
redirectsFiles: [redirectsPath],
minimal: true,
});
config.redirects = redirects;
const { headers } = await parseAllHeaders({
configHeaders: config.headers,
headersFiles: [headersPath],
minimal: true,
});
config.headers = headers;
await uploadDeployBlobs({
deployId,
siteId,
silent,
options,
cachedConfig: command.netlify.cachedConfig,
packagePath: command.workspacePackage,
});
results = await deploySite(command, api, siteId, deployFolder, {
// @ts-expect-error FIXME
config,
fnDir: functionDirectories,
functionsConfig,
statusCb: silent ? () => { } : deployProgressCb(),
deployTimeout,
syncFileLimit: SYNC_FILE_LIMIT,
// pass an existing deployId to update
deployId,
filter: getDeployFilesFilter({ site, deployFolder }),
workingDir: command.workingDir,
manifestPath,
skipFunctionsCache,
siteRoot: site.root,
});
}
catch (error) {
if (deployId) {
await cancelDeploy({ api, deployId });
}
return reportDeployError({ error: error, failAndExit: logAndThrowError });
}
const siteUrl = results.deploy.ssl_url || results.deploy.url;
const deployUrl = results.deploy.deploy_ssl_url || results.deploy.deploy_url;
const logsUrl = `${results.deploy.admin_url}/deploys/${results.deploy.id}`;
let functionLogsUrl = `${results.deploy.admin_url}/logs/functions`;
let edgeFunctionLogsUrl = `${results.deploy.admin_url}/logs/edge-functions`;
if (!deployToProduction) {
functionLogsUrl += `?scope=deploy:${deployId}`;
edgeFunctionLogsUrl += `?scope=deployid:${deployId}`;
}
return {
siteId: results.deploy.site_id,
siteName: results.deploy.name,
deployId: results.deployId,
siteUrl,
deployUrl,
logsUrl,
functionLogsUrl,
edgeFunctionLogsUrl,
sourceZipFileName: uploadSourceZipResult?.sourceZipFileName,
};
};
const handleBuild = async ({ cachedConfig, currentDir, defaultConfig, deployHandler, deployId, options, packagePath, skewProtectionToken, }) => {
if (!options.build) {
return {};
}
const [token] = await getToken();
const resolvedOptions = await getRunBuildOptions({
cachedConfig,
currentDir,
defaultConfig,
deployHandler,
deployId,
options,
packagePath,
skewProtectionToken,
token,
});
const { configMutations, exitCode, newConfig, logs } = await runBuild(resolvedOptions);
// Without this, the deploy command fails silently
if (exitCode !== 0) {
let message = '';
if (options.verbose && logs?.stdout.length) {
message += `\n\n${logs.stdout.join('')}\n\n`;
}
if (logs?.stderr.length) {
message += logs.stderr.join('');
}
logAndThrowError(`Error while running build${message}`);
}
return { newConfig, configMutations };
};
const bundleEdgeFunctions = async (options, command) => {
const argv = process.argv.slice(2);
const statusCb = options.silent || argv.includes('--json') || argv.includes('--silent') ? () => { } : deployProgressCb();
statusCb({
type: 'edge-functions-bundling',
msg: 'Bundling edge functions...\n',
phase: 'start',
});
const { severityCode, success } = await runCoreSteps(['edge_functions_bundling'], {
...options,
packagePath: command.workspacePackage,
buffer: true,
featureFlags: edgeFunctionsFeatureFlags,
// We log our own progress so we don't want this as well. Plus, this logs much of the same
// information as the build that (likely) came before this as part of the deploy build.
quiet: options.debug ?? true,
// (cachedConfig type error hides this one, but it still is valid) @ts-expect-error FIXME(serhalp): This is missing from the `runCoreSteps` type in @netlify/build
edgeFunctionsBootstrapURL: await getBootstrapURL(),
// @ts-expect-error 'CachedConfig' is not assignable to type 'Record<string, unknown>'.
// Index signature for type 'string' is missing in type 'CachedConfig'.
cachedConfig: command.netlify.cachedConfig,
});
if (!success) {
statusCb({
type: 'edge-functions-bundling',
msg: 'Deploy aborted due to error while bundling edge functions',
phase: 'error',
});
exit(severityCode);
}
statusCb({
type: 'edge-functions-bundling',
msg: 'Finished bundling edge functions',
phase: 'stop',
});
};
const printResults = ({ deployToProduction, uploadSourceZip, json, results, runBuildCommand, }) => {
const msgData = {
'Build logs': terminalLink(results.logsUrl, results.logsUrl, { fallback: false }),
'Function logs': terminalLink(results.functionLogsUrl, results.functionLogsUrl, { fallback: false }),
'Edge function Logs': terminalLink(results.edgeFunctionLogsUrl, results.edgeFunctionLogsUrl, { fallback: false }),
};
log('');
// Note: this is leakily mimicking the @netlify/build heading style
log(chalk.cyanBright.bold(`🚀 Deploy complete\n${'─'.repeat(64)}`));
// Json response for piping commands
if (json) {
const jsonData = {
site_id: results.siteId,
site_name: results.siteName,
deploy_id: results.deployId,
deploy_url: results.deployUrl,
logs: results.logsUrl,
function_logs: results.functionLogsUrl,
edge_function_logs: results.edgeFunctionLogsUrl,
};
if (deployToProduction) {
jsonData.url = results.siteUrl;
}
if (uploadSourceZip) {
jsonData.source_zip_filename = results.sourceZipFileName;
}
logJson(jsonData);
exit(0);
}
else {
const message = deployToProduction
? `Deployed to production URL: ${terminalLink(results.siteUrl, results.siteUrl, { fallback: false })}\n
Unique deploy URL: ${terminalLink(results.deployUrl, results.deployUrl, { fallback: false })}`
: `Deployed draft to ${terminalLink(results.deployUrl, results.deployUrl, { fallback: false })}`;
log(boxen(message, {
padding: 1,
margin: 1,
textAlignment: 'center',
borderStyle: 'round',
borderColor: NETLIFY_CYAN_HEX,
// This is an intentional half-width space to work around a unicode padding math bug in boxen
// eslint-disable-next-line no-irregular-whitespace
title: `⬥ ${deployToProduction ? 'Production deploy' : 'Draft deploy'} is live ⬥ `,
titleAlignment: 'center',
}));
log(prettyjson.render(msgData));
if (!deployToProduction) {
log();
log('If everything looks good on your draft URL, deploy it to your main project URL with the --prod flag:');
log(chalk.cyanBright.bold(`netlify deploy${runBuildCommand ? '' : ' --no-build'} --prod`));
log();
}
}
};
const prepAndRunDeploy = async ({ api, command, config, deployToProduction, options, site, siteData, siteId, workingDir, deployId, }) => {
const alias = options.alias || options.branch;
// if a context is passed besides dev, we need to pull env vars from that specific context
if (options.context && options.context !== 'dev') {
command.netlify.cachedConfig.env = await getEnvelopeEnv({
api,
context: options.context,
env: command.netlify.cachedConfig.env,
siteInfo: siteData,
});
}
const deployFolder = await getDeployFolder({ command, options, config, site, siteData });
const functionsFolder = getFunctionsFolder({ workingDir, options, config, site, siteData });
const { configPath } = site;
// build flag wasn't used and edge functions directories exist
if (!options.build && (await anyEdgeFunctionsDirectoryExists(command))) {
// for the case of directories existing but not containing any edge functions,
// there is early bail in edge functions bundling after scanning for edge functions
// for this case and to avoid replicating scanning logic here, we defer to the bundling step
await bundleEdgeFunctions(options, command);
}
log('');
// Note: this is leakily mimicking the @netlify/build heading style
log(chalk.cyanBright.bold(`Deploying to Netlify\n${'─'.repeat(64)}`));
log('');
log(prettyjson.render({
'Deploy path': deployFolder,
'Functions path': functionsFolder,
'Configuration path': configPath,
}));
log();
const { functionsFolderStat } = await validateFolders({
deployFolder,
functionsFolder,
});
const siteEnv = await getEnvelopeEnv({
api,
context: options.context,
env: command.netlify.cachedConfig.env,
raw: true,
scope: 'functions',
siteInfo: siteData,
});
const functionsConfig = normalizeFunctionsConfig({
functionsConfig: config.functions,
projectRoot: site.root,
siteEnv,
});
const results = await runDeploy({
// @ts-expect-error FIXME
alias,
api,
command,
config,
deployFolder,
deployTimeout: options.timeout ? options.timeout * SEC_TO_MILLISEC : DEFAULT_DEPLOY_TIMEOUT,
deployToProduction,
functionsConfig,
// pass undefined functionsFolder if doesn't exist
functionsFolder: functionsFolderStat && functionsFolder,
options,
packagePath: command.workspacePackage,
silent: options.json || options.silent,
site,
siteData,
siteId,
skipFunctionsCache: options.skipFunctionsCache,
title: options.message,
deployId,
});
return results;
};
const validateTeamForSiteCreation = (accounts, options, siteName) => {
if (accounts.length === 0) {
return logAndThrowError('No teams available. Please ensure you have access to at least one team.');
}
if (accounts.length === 1) {
options.team = accounts[0].slug;
const message = siteName ? `Creating new site: ${siteName}` : 'Creating new site with random name';
log(`${message} (using team: ${accounts[0].name})`);
return;
}
const availableTeams = accounts.map((team) => team.slug).join(', ');
return logAndThrowError(`Multiple teams available. Please specify which team to use with --team flag.\n` +
`Available teams: ${availableTeams}\n\n` +
`Example: netlify deploy --create-site${siteName ? ` ${siteName}` : ''} --team <TEAM_SLUG>`);
};
const createSiteWithFlags = async (options, command, site) => {
const { accounts } = command.netlify;
const siteName = typeof options.createSite === 'string' ? options.createSite : undefined;
if (!options.team) {
validateTeamForSiteCreation(accounts, options, siteName);
}
else {
const message = siteName ? `Creating new site: ${siteName}` : 'Creating new site with random name';
log(message);
}
// Create site directly via API to bypass interactive prompts
const { api } = command.netlify;
const body = {};
if (siteName) {
body.name = siteName.trim();
}
if (!options.team) {
throw new Error('Team must be specified to create a site');
}
try {
const siteData = await api.createSiteInTeam({
accountSlug: options.team,
body,
});
site.id = siteData.id;
return siteData;
}
catch (error_) {
if (error_.status === 422) {
return logAndThrowError(siteName
? `Site name "${siteName}" is already taken. Please try a different name.`
: 'Unable to create site with a random name. Please try again or specify a different name.');
}
return logAndThrowError(`Failed to create site: ${error_.status}: ${error_.message}`);
}
};
const promptForSiteAction = async (options, command, site) => {
log("This folder isn't linked to a project yet");
const { accounts } = command.netlify;
const availableTeams = accounts.map((acc) => ({ name: acc.name, slug: acc.slug }));
const copyableCommand = generateDeployCommand(options, availableTeams, command);
log(`\nTo create and deploy in one go, use: ${copyableCommand}`);
if (availableTeams.length > 1) {
log(`\nYou must pick a --team: ${availableTeams.map((team) => team.slug).join(', ')}`);
}
const { initChoice } = await inquirer.prompt([
{
type: 'list',
name: 'initChoice',
message: 'What would you like to do?',
choices: [
{
name: '⇄ Link this directory to an existing project',
value: 'link',
},
{
name: '+ Create & configure a new project',
value: 'create',
},
],
},
]);
const siteData = initChoice === 'create' ? await sitesCreate({}, command) : await link({}, command);
site.id = siteData.id;
return siteData;
};
const ensureSiteExists = async (options, command, site, siteInfo) => {
const hasSiteData = (site.id || options.site) && !isEmpty(siteInfo);
if (hasSiteData) {
return siteInfo;
}
if (options.createSite) {
return createSiteWithFlags(options, command, site);
}
return promptForSiteAction(options, command, site);
};
export const deploy = async (options, command) => {
const { workingDir } = command;
const { api, site, siteInfo } = command.netlify;
const alias = options.alias || options.branch;
command.setAnalyticsPayload({ open: options.open, prod: options.prod, json: options.json, alias: Boolean(alias) });
await command.authenticate(options.auth);
const siteData = await ensureSiteExists(options, command, site, siteInfo);
const siteId = siteData.id;
if (options.trigger) {
return triggerDeploy({ api, options, siteData, siteId });
}
const deployToProduction = !options.draft && (options.prod || (options.prodIfUnlocked && !(siteData.published_deploy?.locked ?? false)));
let results = {};
if (options.build) {
if (deployToProduction) {
await prepareProductionDeploy({ siteData, api, options, command });
}
const draft = options.draft || (!deployToProduction && !alias);
const createDeployBody = { draft, branch: alias, include_upload_url: options.uploadSourceZip };
// TODO: Type this properly in `@netlify/api`.
const deployMetadata = (await api.createSiteDeploy({
siteId,
title: options.message,
body: createDeployBody,
}));
const deployId = deployMetadata.id || '';
const skewProtectionToken = deployMetadata.skew_protection_token;
let sourceZipFileName;
if (options.uploadSourceZip &&
deployMetadata.source_zip_upload_url &&
deployMetadata.source_zip_filename &&
site.root) {
await uploadSourceZip({
sourceDir: site.root,
uploadUrl: deployMetadata.source_zip_upload_url,
filename: deployMetadata.source_zip_filename,
statusCb: options.json || options.silent ? () => { } : deployProgressCb(),
});
sourceZipFileName = deployMetadata.source_zip_filename;
}
try {
const settings = await detectFrameworkSettings(command, 'build');
await handleBuild({
packagePath: command.workspacePackage,
cachedConfig: command.netlify.cachedConfig,
defaultConfig: getDefaultConfig(settings),
currentDir: command.workingDir,
options,
deployHandler: async ({ netlifyConfig }) => {
results = await prepAndRunDeploy({
command,
options,
workingDir,
api,
site,
config: netlifyConfig,
siteData,
siteId,
deployToProduction,
deployId,
});
return {};
},
deployId,
skewProtectionToken,
});
// Ensure source zip filename is included in results for JSON output
if (sourceZipFileName) {
results.sourceZipFileName = sourceZipFileName;
}
}
catch (error) {
// The build has failed, so let's cancel the deploy we created.
await cancelDeploy({ api, deployId });
throw error;
}
}
else {
results = await prepAndRunDeploy({
command,
options,
workingDir,
api,
site,
config: command.netlify.config,
siteData,
siteId,
deployToProduction,
});
}
printResults({
runBuildCommand: options.build,
json: options.json,
results,
deployToProduction,
uploadSourceZip: !!options.uploadSourceZip,
});
if (options.open) {
const urlToOpen = deployToProduction ? results.siteUrl : results.deployUrl;
await openBrowser({ url: urlToOpen });
exit();
}
};
//# sourceMappingURL=deploy.js.map