291 lines
No EOL
14 KiB
JavaScript
291 lines
No EOL
14 KiB
JavaScript
import chalk from "chalk";
|
|
import { spawn } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import ora from "ora";
|
|
import path from "node:path";
|
|
import { findSWAConfigFile } from "../../../core/utils/user-config.js";
|
|
import { getCurrentSwaCliConfigFromFile, updateSwaCliConfigFile } from "../../../core/utils/cli-config.js";
|
|
import { logger, logGitHubIssueMessageAndExit } from "../../../core/utils/logger.js";
|
|
import { isUserOrConfigOption } from "../../../core/utils/options.js";
|
|
import { readWorkflowFile } from "../../../core/utils/workflow-config.js";
|
|
import { chooseOrCreateProjectDetails, getStaticSiteDeployment } from "../../../core/account.js";
|
|
import { DEFAULT_RUNTIME_LANGUAGE } from "../../../core/constants.js";
|
|
import { cleanUp, getDeployClientPath } from "../../../core/deploy-client.js";
|
|
import { swaCLIEnv } from "../../../core/env.js";
|
|
import { getDefaultVersion } from "../../../core/functions-versions.js";
|
|
import { login } from "../login/login.js";
|
|
import { loadPackageJson } from "../../../core/utils/json.js";
|
|
const packageInfo = loadPackageJson();
|
|
export async function deploy(options) {
|
|
const { SWA_CLI_DEPLOYMENT_TOKEN, SWA_CLI_DEBUG } = swaCLIEnv();
|
|
const isVerboseEnabled = SWA_CLI_DEBUG === "silly";
|
|
let { appLocation, apiLocation, dataApiLocation, outputLocation, dryRun, deploymentToken, printToken, appName, swaConfigLocation, verbose, apiLanguage, apiVersion, } = options;
|
|
if (dryRun) {
|
|
logger.warn("***********************************************************************");
|
|
logger.warn("* WARNING: Running in dry run mode. This project will not be deployed *");
|
|
logger.warn("***********************************************************************");
|
|
logger.warn("");
|
|
}
|
|
// make sure appLocation is set
|
|
appLocation = path.resolve(appLocation || process.cwd());
|
|
// make sure dataApiLocation is set
|
|
if (dataApiLocation) {
|
|
dataApiLocation = path.resolve(dataApiLocation);
|
|
if (!fs.existsSync(dataApiLocation)) {
|
|
logger.error(`The provided Data API folder ${dataApiLocation} does not exist. Abort.`, true);
|
|
return;
|
|
}
|
|
else {
|
|
logger.log(`Deploying Data API from folder:`);
|
|
logger.log(` ${chalk.green(dataApiLocation)}`);
|
|
logger.log(``);
|
|
}
|
|
}
|
|
logger.silly(`Resolving outputLocation=${outputLocation} full path...`);
|
|
let resolvedOutputLocation = path.resolve(appLocation, outputLocation);
|
|
// if folder exists, deploy from a specific build folder (outputLocation), relative to appLocation
|
|
if (!fs.existsSync(resolvedOutputLocation)) {
|
|
if (!fs.existsSync(outputLocation)) {
|
|
logger.error(`The folder "${resolvedOutputLocation}" is not found. Exit.`, true);
|
|
return;
|
|
}
|
|
// otherwise, build folder (outputLocation) is using the absolute location
|
|
resolvedOutputLocation = path.resolve(outputLocation);
|
|
}
|
|
logger.log(`Deploying front-end files from folder:`);
|
|
logger.log(` ${chalk.green(resolvedOutputLocation)}`);
|
|
logger.log(``);
|
|
// if --api-location is provided, use it as the api folder
|
|
let resolvedApiLocation = undefined;
|
|
if (apiLocation) {
|
|
resolvedApiLocation = path.resolve(apiLocation);
|
|
if (!fs.existsSync(resolvedApiLocation)) {
|
|
logger.error(`The provided API folder ${resolvedApiLocation} does not exist. Abort.`, true);
|
|
return;
|
|
}
|
|
else {
|
|
logger.log(`Deploying API from folder:`);
|
|
logger.log(` ${chalk.green(resolvedApiLocation)}`);
|
|
logger.log(``);
|
|
}
|
|
}
|
|
else {
|
|
// otherwise, check if the default api folder exists and print a warning
|
|
const apiFolder = await findApiFolderInPath(appLocation);
|
|
if (apiFolder) {
|
|
logger.warn(`An API folder was found at ".${path.sep + path.basename(apiFolder)}" but the --api-location option was not provided. The API will not be deployed.\n`);
|
|
}
|
|
}
|
|
if (!isUserOrConfigOption("apiLanguage")) {
|
|
logger.log(`Consider providing api-language and version using --api-language and --api-version flags,
|
|
otherwise default values apiLanguage: ${apiLanguage} and apiVersion: ${apiVersion} will apply`);
|
|
}
|
|
else if (!isUserOrConfigOption("apiVersion")) {
|
|
if (!apiLanguage) {
|
|
apiLanguage = DEFAULT_RUNTIME_LANGUAGE;
|
|
}
|
|
apiVersion = getDefaultVersion(apiLanguage);
|
|
logger.log(`Api language "${apiLanguage}" is provided but api version is not provided.
|
|
Assuming default version "${apiVersion}"`);
|
|
}
|
|
// resolve the deployment token
|
|
if (deploymentToken) {
|
|
deploymentToken = deploymentToken;
|
|
logger.silly("Deployment token provided via flag");
|
|
logger.silly({ [chalk.green(`--deployment-token`)]: deploymentToken });
|
|
}
|
|
else if (SWA_CLI_DEPLOYMENT_TOKEN) {
|
|
deploymentToken = SWA_CLI_DEPLOYMENT_TOKEN;
|
|
logger.silly("Deployment token found in Environment Variables:");
|
|
logger.silly({ [chalk.green(`SWA_CLI_DEPLOYMENT_TOKEN`)]: SWA_CLI_DEPLOYMENT_TOKEN });
|
|
}
|
|
else if (dryRun === false) {
|
|
logger.silly(`No deployment token found. Trying interactive login...`);
|
|
try {
|
|
const { credentialChain, subscriptionId } = await login({
|
|
...options,
|
|
});
|
|
logger.silly(`Login successful`);
|
|
if (appName) {
|
|
logger.log(`\nChecking project "${appName}" settings...`);
|
|
}
|
|
else {
|
|
logger.log(`\nChecking project settings...`);
|
|
}
|
|
const { resourceGroup, staticSiteName } = (await chooseOrCreateProjectDetails(options, credentialChain, subscriptionId, printToken));
|
|
logger.silly(`Project settings:`);
|
|
logger.silly({
|
|
resourceGroup,
|
|
staticSiteName,
|
|
subscriptionId,
|
|
});
|
|
const deploymentTokenResponse = await getStaticSiteDeployment(credentialChain, subscriptionId, resourceGroup, staticSiteName);
|
|
deploymentToken = deploymentTokenResponse?.properties?.apiKey;
|
|
if (!deploymentToken) {
|
|
logger.error("Cannot find a deployment token. Aborting.", true);
|
|
}
|
|
else {
|
|
logger.log(chalk.green(`✔ Successfully setup project!`));
|
|
// store project settings in swa-cli.config.json (if available)
|
|
if (dryRun === false) {
|
|
const currentSwaCliConfig = getCurrentSwaCliConfigFromFile();
|
|
if (currentSwaCliConfig?.config) {
|
|
logger.silly(`Saving project settings to swa-cli.config.json...`);
|
|
const newConfig = { ...currentSwaCliConfig?.config };
|
|
newConfig.appName = staticSiteName;
|
|
newConfig.resourceGroup = resourceGroup;
|
|
updateSwaCliConfigFile(newConfig);
|
|
}
|
|
else {
|
|
logger.silly(`No swa-cli.config.json file found. Skipping saving project settings.`);
|
|
}
|
|
}
|
|
logger.silly("\nDeployment token provided via remote configuration");
|
|
logger.silly({ [chalk.green(`deploymentToken`)]: deploymentToken });
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger.error(error.message);
|
|
return;
|
|
}
|
|
}
|
|
logger.log(`\nDeploying to environment: ${chalk.green(options.env)}\n`);
|
|
if (printToken) {
|
|
logger.log(`Deployment token:`);
|
|
logger.log(chalk.green(deploymentToken));
|
|
process.exit(0);
|
|
}
|
|
// TODO: do that in options
|
|
// mix CLI args with the project's build workflow configuration (if any)
|
|
// use any specific workflow config that the user might provide under ".github/workflows/"
|
|
// Note: CLI args will take precedence over workflow config
|
|
let userWorkflowConfig = {
|
|
appLocation,
|
|
outputLocation: resolvedOutputLocation,
|
|
apiLocation: resolvedApiLocation,
|
|
dataApiLocation,
|
|
};
|
|
try {
|
|
userWorkflowConfig = readWorkflowFile({
|
|
userWorkflowConfig,
|
|
});
|
|
}
|
|
catch (err) {
|
|
logger.warn(``);
|
|
logger.warn(`Error reading workflow configuration:`);
|
|
logger.warn(err.message);
|
|
logger.warn(`See https://docs.microsoft.com/azure/static-web-apps/build-configuration?tabs=github-actions#build-configuration for more information.`);
|
|
}
|
|
swaConfigLocation = swaConfigLocation || userWorkflowConfig?.appLocation;
|
|
const swaConfigFilePath = (await findSWAConfigFile(swaConfigLocation))?.filepath;
|
|
const resolvedSwaConfigLocation = swaConfigFilePath ? path.dirname(swaConfigFilePath) : undefined;
|
|
const cliEnv = {
|
|
SWA_CLI_DEBUG: verbose,
|
|
SWA_RUNTIME_WORKFLOW_LOCATION: userWorkflowConfig?.files?.[0],
|
|
SWA_RUNTIME_CONFIG_LOCATION: resolvedSwaConfigLocation,
|
|
SWA_RUNTIME_CONFIG: swaConfigFilePath,
|
|
SWA_CLI_VERSION: packageInfo.version,
|
|
SWA_CLI_DEPLOY_DRY_RUN: `${dryRun}`,
|
|
SWA_CLI_DEPLOY_BINARY: undefined,
|
|
};
|
|
const deployClientEnv = {
|
|
DEPLOYMENT_ACTION: options.dryRun ? "close" : "upload",
|
|
DEPLOYMENT_PROVIDER: "SwaCli",
|
|
REPOSITORY_BASE: userWorkflowConfig?.appLocation,
|
|
SKIP_APP_BUILD: "true",
|
|
SKIP_API_BUILD: "true",
|
|
DEPLOYMENT_TOKEN: deploymentToken,
|
|
// /!\ Static site client doesn't use OUTPUT_LOCATION at all if SKIP_APP_BUILD is set,
|
|
// so you need to provide the output path as the app location
|
|
APP_LOCATION: userWorkflowConfig?.outputLocation,
|
|
// OUTPUT_LOCATION: outputLocation,
|
|
API_LOCATION: userWorkflowConfig?.apiLocation,
|
|
DATA_API_LOCATION: userWorkflowConfig?.dataApiLocation,
|
|
// If config file is not in output location, we need to tell where to find it
|
|
CONFIG_FILE_LOCATION: resolvedSwaConfigLocation,
|
|
VERBOSE: isVerboseEnabled ? "true" : "false",
|
|
FUNCTION_LANGUAGE: apiLanguage,
|
|
FUNCTION_LANGUAGE_VERSION: apiVersion,
|
|
};
|
|
// set the DEPLOYMENT_ENVIRONMENT env variable only when the user has provided
|
|
// a deployment environment which is not "production".
|
|
if (options.env?.toLowerCase() !== "production" && options.env?.toLowerCase() !== "prod") {
|
|
deployClientEnv.DEPLOYMENT_ENVIRONMENT = options.env;
|
|
}
|
|
logger.log(`Deploying project to Azure Static Web Apps...`);
|
|
let spinner = {};
|
|
try {
|
|
const { binary, buildId } = await getDeployClientPath();
|
|
if (binary) {
|
|
spinner = ora();
|
|
cliEnv.SWA_CLI_DEPLOY_BINARY = `${binary}@${buildId}`;
|
|
spinner.text = `Deploying using ${cliEnv.SWA_CLI_DEPLOY_BINARY}`;
|
|
logger.silly(`Deploying using ${cliEnv.SWA_CLI_DEPLOY_BINARY}`);
|
|
logger.silly(`Deploying using the following options:`);
|
|
logger.silly({ env: { ...cliEnv, ...deployClientEnv } });
|
|
spinner.start(`Preparing deployment. Please wait...`);
|
|
const child = spawn(binary, [], {
|
|
env: {
|
|
...swaCLIEnv(cliEnv, deployClientEnv),
|
|
},
|
|
});
|
|
let projectUrl = "";
|
|
child.stdout.on("data", (data) => {
|
|
data
|
|
.toString()
|
|
.trim()
|
|
.split("\n")
|
|
.forEach((line) => {
|
|
if (line.includes("Exiting")) {
|
|
spinner.text = line;
|
|
spinner.stop();
|
|
}
|
|
else if (line.includes("Visit your site at:")) {
|
|
projectUrl = line.match("http.*")?.pop()?.trim();
|
|
line = "";
|
|
}
|
|
// catch errors printed to stdout
|
|
else if (line.includes("[31m")) {
|
|
if (line.includes("Cannot deploy to the function app because Function language info isn't provided.")) {
|
|
line = chalk.red(`Cannot deploy to the function app because Function language info isn't provided, use flags "--api-language" and "--api-version" or add a "platform.apiRuntime" property to your staticwebapp.config.json file, or create one in ${options.outputLocation}. Please consult the documentation for more information about staticwebapp.config.json: https://learn.microsoft.com/azure/static-web-apps/build-configuration?tabs=github-actions#skip-building-the-api`);
|
|
}
|
|
spinner.fail(chalk.red(line));
|
|
}
|
|
else {
|
|
if (isVerboseEnabled || dryRun) {
|
|
spinner.info(line.trim());
|
|
}
|
|
else {
|
|
spinner.text = line.trim();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
child.on("error", (error) => {
|
|
logger.error(error.toString());
|
|
});
|
|
child.on("close", (code) => {
|
|
cleanUp();
|
|
if (code === 0) {
|
|
spinner.succeed(chalk.green(`Project deployed to ${chalk.underline(projectUrl)} 🚀`));
|
|
logger.log(``);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger.error("");
|
|
logger.error("Deployment Failed :(");
|
|
logger.error(`Deployment Failure Reason: ${error.message}`);
|
|
logger.error(`For further information, please visit the Azure Static Web Apps documentation at https://docs.microsoft.com/azure/static-web-apps/`);
|
|
logGitHubIssueMessageAndExit();
|
|
}
|
|
finally {
|
|
cleanUp();
|
|
}
|
|
}
|
|
async function findApiFolderInPath(appPath) {
|
|
const entries = await fs.promises.readdir(appPath, { withFileTypes: true });
|
|
return entries.find((entry) => entry.name.toLowerCase() === "api" && entry.isDirectory())?.name;
|
|
}
|
|
//# sourceMappingURL=deploy.js.map
|