@azure/static-web-apps-cli
Version:
Azure Static Web Apps CLI
284 lines • 13.2 kB
JavaScript
import { WebSiteManagementClient } from "@azure/arm-appservice";
import { ResourceManagementClient } from "@azure/arm-resources";
import { SubscriptionClient } from "@azure/arm-subscriptions";
import { AzureCliCredential, ChainedTokenCredential, ClientSecretCredential, DeviceCodeCredential, EnvironmentCredential, InteractiveBrowserCredential, useIdentityPlugin, } from "@azure/identity";
import chalk from "chalk";
import ora from "ora";
import path from "node:path";
import { swaCLIEnv } from "./env.js";
import { chooseProjectName, chooseProjectSku, chooseStaticSite, wouldYouLikeToCreateStaticSite, wouldYouLikeToOverrideStaticSite, } from "./prompts.js";
import { swaCliPersistencePlugin } from "./swa-cli-persistence-plugin/index.js";
import { SWACLIPersistenceCachePlugin } from "./swa-cli-persistence-plugin/persistence-cache-plugin.js";
import { logger } from "./utils/logger.js";
import { dasherize } from "./utils/strings.js";
import { isRunningInDocker } from "./utils/docker.js";
const DEFAULT_AZURE_LOCATION = "West US 2";
export async function authenticateWithAzureIdentity(details = {}, useKeychain = true, clearCache = false) {
logger.silly("Executing authenticateWithAzureIdentity");
logger.silly({ details, useKeychain });
let tokenCachePersistenceOptions = {
enabled: false,
name: "swa-cli-persistence-plugin",
unsafeAllowUnencryptedStorage: false,
};
if (useKeychain === true) {
logger.silly("Keychain is enabled");
useIdentityPlugin(swaCliPersistencePlugin);
tokenCachePersistenceOptions.enabled = true;
if (clearCache) {
logger.silly("Clearing keychain credentials");
await new SWACLIPersistenceCachePlugin(tokenCachePersistenceOptions).clearCache();
}
}
else {
logger.silly("Keychain is disabled");
tokenCachePersistenceOptions.enabled = false;
}
const browserCredential = new InteractiveBrowserCredential({
redirectUri: `http://localhost:31337`,
tokenCachePersistenceOptions,
tenantId: details.tenantId,
});
const deviceCredential = new DeviceCodeCredential({
tokenCachePersistenceOptions,
tenantId: details.tenantId,
});
const environmentCredential = new EnvironmentCredential();
const azureCliCredential = new AzureCliCredential({
tenantId: details.tenantId,
});
// Only use interactive browser credential if we're not running in docker
const credentials = isRunningInDocker()
? [environmentCredential, azureCliCredential, deviceCredential]
: [environmentCredential, azureCliCredential, browserCredential, deviceCredential];
if (details.tenantId && details.clientId && details.clientSecret) {
const clientSecretCredential = new ClientSecretCredential(details.tenantId, details.clientId, details.clientSecret, {
tokenCachePersistenceOptions,
});
// insert at the beginning of the array to ensure that it is tried first
credentials.unshift(clientSecretCredential);
}
return new ChainedTokenCredential(...credentials);
}
async function isResourceGroupExists(resourceGroup, subscriptionId, credentialChain) {
const client = new ResourceManagementClient(credentialChain, subscriptionId);
try {
const rg = await client.resourceGroups.checkExistence(resourceGroup);
return rg.body;
}
catch (error) {
if (error?.code?.includes("ResourceGroupNotFound")) {
return false;
}
throw new Error(error);
}
}
async function createResourceGroup(resourceGroup, credentialChain, subscriptionId) {
const client = new ResourceManagementClient(credentialChain, subscriptionId);
const { AZURE_REGION_LOCATION } = swaCLIEnv();
const resourceGroupEnvelope = {
location: AZURE_REGION_LOCATION || DEFAULT_AZURE_LOCATION,
};
const result = await client.resourceGroups.createOrUpdate(resourceGroup, resourceGroupEnvelope);
logger.silly(result);
return result;
}
async function gracefullyFail(promise, errorCode) {
try {
return await promise;
}
catch (error) {
if (errorCode === undefined || (error.statusCode && errorCode === error.statusCode)) {
logger.silly(`Caught error: ${error.message}`);
return undefined;
}
throw error;
}
}
async function createStaticSite(options, credentialChain, subscriptionId) {
let { appName, resourceGroup } = options;
const maxProjectNameLength = 63; // azure convention is 64 characters (zero-indexed)
const defaultStaticSiteName = appName || dasherize(path.basename(process.cwd())).substring(0, maxProjectNameLength);
appName = await chooseProjectName(defaultStaticSiteName, maxProjectNameLength);
const sku = await chooseProjectSku();
resourceGroup = resourceGroup || `${appName}-rg`;
let spinner = ora("Creating a new project...").start();
// if the resource group does not exist, create it
if ((await isResourceGroupExists(resourceGroup, subscriptionId, credentialChain)) === false) {
logger.silly(`Resource group "${resourceGroup}" does not exist. Creating one...`);
// create the resource group
await createResourceGroup(resourceGroup, credentialChain, subscriptionId);
}
// create the static web app instance
try {
const websiteClient = new WebSiteManagementClient(credentialChain, subscriptionId);
const { AZURE_REGION_LOCATION } = swaCLIEnv();
const staticSiteEnvelope = {
location: AZURE_REGION_LOCATION || DEFAULT_AZURE_LOCATION,
sku: { name: sku, tier: sku },
// these are mandatory, otherwise the static site will not be created
buildProperties: {
appLocation: "",
outputLocation: "",
apiLocation: "",
},
};
logger.silly(`Checking if project "${appName}" already exists...`);
// check if the static site already exists
const project = await gracefullyFail(websiteClient.staticSites.getStaticSite(resourceGroup, appName), 404);
const projectExists = project !== undefined;
if (projectExists) {
spinner.stop();
const confirm = await wouldYouLikeToOverrideStaticSite?.(appName);
if (confirm === false) {
return (await chooseOrCreateStaticSite(options, credentialChain, subscriptionId));
}
}
if (projectExists) {
spinner.start(`Updating project "${appName}"...`);
}
logger.silly(`Creating static site "${appName}" in resource group "${resourceGroup}"...`);
const result = await websiteClient.staticSites.beginCreateOrUpdateStaticSiteAndWait(resourceGroup, appName, staticSiteEnvelope);
logger.silly(`Static site "${appName}" created successfully.`);
logger.silly(result);
if (result.id) {
if (projectExists) {
spinner.succeed(`Project "${appName}" updated successfully.`);
}
else {
spinner.succeed(chalk.green("Project created successfully!"));
}
}
return result;
}
catch (error) {
spinner.fail(chalk.red("Project creation failed."));
logger.error(error.message, true);
return undefined;
}
}
async function chooseOrCreateStaticSite(options, credentialChain, subscriptionId) {
const staticSites = await listStaticSites(credentialChain, subscriptionId);
// 1- when there are no static sites
if (staticSites.length === 0) {
const confirm = await wouldYouLikeToCreateStaticSite();
if (confirm) {
return (await createStaticSite(options, credentialChain, subscriptionId));
}
else {
logger.error("No projects found. Create a new project and try again.", true);
}
}
// 2- when there is only one static site
else if (staticSites.length === 1) {
logger.silly("Only one project found. Trying to use it if the name matches...");
const staticSite = staticSites[0];
if (options.appName === staticSite.name) {
return staticSite;
}
else {
// if the name doesn't match, ask the user if they want to create a new project
const confirm = await wouldYouLikeToCreateStaticSite();
if (confirm) {
return (await createStaticSite(options, credentialChain, subscriptionId));
}
else {
logger.error(`The provided project name "${options.appName}" was not found.`, true);
}
}
}
// 3- when there are multiple static sites
if (options.appName) {
// if the user provided a project name, try to find it and use it
logger.silly(`Looking for project "${options.appName}"...`);
const staticSite = staticSites.find((s) => s.name === options.appName);
if (staticSite) {
return staticSite;
}
}
// otherwise, ask the user to choose one
const staticSite = await chooseStaticSite(staticSites, options.appName);
if (staticSite === "NEW") {
// if the user chose to create a new project, switch to the create project flow
return (await createStaticSite(options, credentialChain, subscriptionId));
}
return staticSites.find((s) => s.name === staticSite);
}
export async function chooseOrCreateProjectDetails(options, credentialChain, subscriptionId, shouldPrintToken) {
const staticSite = (await chooseOrCreateStaticSite(options, credentialChain, subscriptionId));
logger.silly("Static site found!");
logger.silly({ staticSite });
if (staticSite && staticSite.id) {
if (!shouldPrintToken && staticSite.provider !== "Custom" && staticSite.provider !== "None" && staticSite.provider !== "SwaCli") {
// TODO: add a temporary warning message until we ship `swa link/unlink`
logger.error(`The project "${staticSite.name}" is linked to "${staticSite.provider}"!`);
logger.error(`Unlink the project from the "${staticSite.provider}" provider and try again.`, true);
return;
}
// in case we have a static site, we will use its resource group and name
// get resource group name from static site id:
// /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/swa-resource-groupe-name/providers/Microsoft.Web/sites/swa-static-site
// 0 / 1 / 2 / 3 / 4
const resourceGroup = staticSite.id.split("/")[4];
const staticSiteName = staticSite.name;
return {
resourceGroup,
staticSiteName,
};
}
else {
logger.error("No project found. Create a new project and try again.", true);
}
return;
}
export async function listTenants(credentialChain) {
const client = new SubscriptionClient(credentialChain);
const tenants = [];
for await (let tenant of client.tenants.list()) {
tenants.push(tenant);
}
return tenants;
}
export async function listResourceGroups(credentialChain, subscriptionId) {
const resourceGroups = [];
const client = new ResourceManagementClient(credentialChain, subscriptionId);
for await (let resource of client.resources.list()) {
resourceGroups.push(resource);
}
return resourceGroups;
}
export async function listSubscriptions(credentialChain) {
const subscriptionClient = new SubscriptionClient(credentialChain);
const subscriptions = [];
for await (let subscription of subscriptionClient.subscriptions.list()) {
subscriptions.push(subscription);
}
return subscriptions;
}
export async function listStaticSites(credentialChain, subscriptionId, resourceGroup) {
const staticSites = [];
const websiteClient = new WebSiteManagementClient(credentialChain, subscriptionId);
let staticSiteList = websiteClient.staticSites.list();
if (resourceGroup) {
staticSiteList = websiteClient.staticSites.listStaticSitesByResourceGroup(resourceGroup);
}
for await (let staticSite of staticSiteList) {
staticSites.push(staticSite);
}
return staticSites.sort((a, b) => a.name?.localeCompare(b.name));
}
export async function getStaticSiteDeployment(credentialChain, subscriptionId, resourceGroup, staticSiteName) {
if (!subscriptionId) {
logger.error("An Azure subscription is required to access your deployment token.", true);
}
if (!resourceGroup) {
logger.error("A resource group is required to access your deployment token.", true);
}
if (!staticSiteName) {
logger.error("A static site name is required to access your deployment token.", true);
}
const websiteClient = new WebSiteManagementClient(credentialChain, subscriptionId);
const deploymentTokenResponse = await websiteClient.staticSites.listStaticSiteSecrets(resourceGroup, staticSiteName);
return deploymentTokenResponse;
}
//# sourceMappingURL=account.js.map