From f37be1e3b9aef93460304fa20feaebe8b6df2195 Mon Sep 17 00:00:00 2001 From: sinedied Date: Mon, 9 Jan 2023 10:49:59 +0100 Subject: [PATCH 1/4] fix: empty spaces uncorrectly detected as positional argument --- src/cli/commands/build.ts | 1 + src/cli/commands/deploy.ts | 1 + src/cli/commands/start.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/cli/commands/build.ts b/src/cli/commands/build.ts index 6a59834c..7a30a68a 100644 --- a/src/cli/commands/build.ts +++ b/src/cli/commands/build.ts @@ -28,6 +28,7 @@ export default function registerCommand(program: Command) { .option("-I, --api-build-command ", "the command used to build your api", DEFAULT_CONFIG.apiBuildCommand) .option("--auto", "automatically detect how to build your app and api", false) .action(async (positionalArg: string | undefined, _options: SWACLIConfig, command: Command) => { + positionalArg = positionalArg?.trim(); const options = await configureOptions(positionalArg, command.optsWithGlobals(), command, "build"); if (positionalArg && !matchLoadedConfigName(positionalArg)) { if (isUserOption("appLocation")) { diff --git a/src/cli/commands/deploy.ts b/src/cli/commands/deploy.ts index 23c4a2c3..eae20a66 100644 --- a/src/cli/commands/deploy.ts +++ b/src/cli/commands/deploy.ts @@ -41,6 +41,7 @@ export default function registerCommand(program: Command) { .option("-pt, --print-token", "print the deployment token", false) .option("--env [environment]", "the type of deployment environment where to deploy the project", DEFAULT_CONFIG.env) .action(async (positionalArg: string | undefined, _options: SWACLIConfig, command: Command) => { + positionalArg = positionalArg?.trim(); const options = await configureOptions(positionalArg, command.optsWithGlobals(), command, "deploy"); if (positionalArg && !matchLoadedConfigName(positionalArg)) { if (isUserOption("outputLocation")) { diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index b49ec964..db581d80 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -63,6 +63,7 @@ export default function registerCommand(program: Command) { .option("-o, --open", "open the browser to the dev server", DEFAULT_CONFIG.open) .option("-f, --func-args ", "pass additional arguments to the func start command") .action(async (positionalArg: string | undefined, _options: SWACLIConfig, command: Command) => { + positionalArg = positionalArg?.trim(); const options = await configureOptions(positionalArg, command.optsWithGlobals(), command, "start"); if (positionalArg && !matchLoadedConfigName(positionalArg)) { // If it's not the config name, it's either output location or dev server url From b5e31c40a58f3f3e32d13f59fe879a616a5ba410 Mon Sep 17 00:00:00 2001 From: sinedied Date: Mon, 9 Jan 2023 11:47:30 +0100 Subject: [PATCH 2/4] test: add unit test --- src/cli/index.spec.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/cli/index.spec.ts b/src/cli/index.spec.ts index 7388b1ac..6609510c 100644 --- a/src/cli/index.spec.ts +++ b/src/cli/index.spec.ts @@ -1,9 +1,15 @@ // Avoid FSREQCALLBACK error jest.mock("./commands/start"); +jest.mock("child_process", () => ({ + execSync: jest.fn(), +})); + import { program } from "commander"; import { UpdateNotifier } from "update-notifier"; +import mockFs from "mock-fs"; import { run } from "./index"; + const pkg = require("../../package.json"); const originalConsoleError = console.error; @@ -16,8 +22,16 @@ describe("cli", () => { console.error = jest.fn(); }); + afterEach(() => { + mockFs.restore(); + const execSyncMock = jest.requireMock("child_process").execSync; + execSyncMock.mockReset(); + }); + afterAll(() => { console.error = originalConsoleError; + jest.resetModules(); + jest.restoreAllMocks(); }); it("should print version", async () => { @@ -53,4 +67,18 @@ describe("cli", () => { " `); }); + + it("should ignore empty spaces when using positional argument", async () => { + const execSyncMock = jest.requireMock("child_process").execSync; + mockFs(); + await run(["node", "swa", "build", " app ", "--app-build-command", "npm run something"]); + expect(execSyncMock.mock.calls[0][1].cwd).toBe("app"); + }); + + it("should not interpret empty spaces as a positional argument", async () => { + const execSyncMock = jest.requireMock("child_process").execSync; + mockFs(); + await run(["node", "swa", "build", " ", "--app-build-command", "npm run something", " "]); + expect(execSyncMock.mock.calls[0][1].cwd).toBe("."); + }); }); From 29739b8729658c4e398570f680c14949b4821d76 Mon Sep 17 00:00:00 2001 From: sinedied Date: Mon, 9 Jan 2023 17:34:29 +0100 Subject: [PATCH 3/4] test: remove unneeded mock --- src/cli/index.spec.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/cli/index.spec.ts b/src/cli/index.spec.ts index 6609510c..835b569a 100644 --- a/src/cli/index.spec.ts +++ b/src/cli/index.spec.ts @@ -1,15 +1,12 @@ -// Avoid FSREQCALLBACK error -jest.mock("./commands/start"); - -jest.mock("child_process", () => ({ - execSync: jest.fn(), -})); - import { program } from "commander"; import { UpdateNotifier } from "update-notifier"; import mockFs from "mock-fs"; import { run } from "./index"; +jest.mock("child_process", () => ({ + execSync: jest.fn(), +})); + const pkg = require("../../package.json"); const originalConsoleError = console.error; @@ -30,8 +27,6 @@ describe("cli", () => { afterAll(() => { console.error = originalConsoleError; - jest.resetModules(); - jest.restoreAllMocks(); }); it("should print version", async () => { From f93b9ae937cfe53f0506bec8ed62e871a635d313 Mon Sep 17 00:00:00 2001 From: sinedied Date: Mon, 9 Jan 2023 18:42:34 +0100 Subject: [PATCH 4/4] refactor: split cli command/register to fix and make testing easier --- src/cli/commands/{ => build}/build.spec.ts | 4 +- src/cli/commands/{ => build}/build.ts | 37 +------ src/cli/commands/build/index.ts | 2 + src/cli/commands/build/register.ts | 32 ++++++ src/cli/commands/{ => deploy}/deploy.spec.ts | 12 +-- src/cli/commands/{ => deploy}/deploy.ts | 74 ++----------- src/cli/commands/deploy/index.ts | 2 + src/cli/commands/deploy/register.ts | 62 +++++++++++ src/cli/commands/docs.ts | 2 +- src/cli/commands/init/index.ts | 2 + src/cli/commands/{ => init}/init.spec.ts | 6 +- src/cli/commands/{ => init}/init.ts | 30 +----- src/cli/commands/init/register.ts | 25 +++++ src/cli/commands/login/index.ts | 2 + src/cli/commands/{ => login}/login.ts | 89 ++++----------- src/cli/commands/login/register.ts | 56 ++++++++++ src/cli/commands/start/index.ts | 2 + src/cli/commands/start/register.ts | 95 ++++++++++++++++ src/cli/commands/{ => start}/start.ts | 107 +------------------ src/cli/index.spec.ts | 16 +-- src/cli/index.ts | 12 +-- 21 files changed, 340 insertions(+), 329 deletions(-) rename src/cli/commands/{ => build}/build.spec.ts (96%) rename src/cli/commands/{ => build}/build.ts (74%) create mode 100644 src/cli/commands/build/index.ts create mode 100644 src/cli/commands/build/register.ts rename src/cli/commands/{ => deploy}/deploy.spec.ts (94%) rename src/cli/commands/{ => deploy}/deploy.ts (78%) create mode 100644 src/cli/commands/deploy/index.ts create mode 100644 src/cli/commands/deploy/register.ts create mode 100644 src/cli/commands/init/index.ts rename src/cli/commands/{ => init}/init.spec.ts (97%) rename src/cli/commands/{ => init}/init.ts (87%) create mode 100644 src/cli/commands/init/register.ts create mode 100644 src/cli/commands/login/index.ts rename src/cli/commands/{ => login}/login.ts (60%) create mode 100644 src/cli/commands/login/register.ts create mode 100644 src/cli/commands/start/index.ts create mode 100644 src/cli/commands/start/register.ts rename src/cli/commands/{ => start}/start.ts (67%) diff --git a/src/cli/commands/build.spec.ts b/src/cli/commands/build/build.spec.ts similarity index 96% rename from src/cli/commands/build.spec.ts rename to src/cli/commands/build/build.spec.ts index ff10fd93..5507c4f6 100644 --- a/src/cli/commands/build.spec.ts +++ b/src/cli/commands/build/build.spec.ts @@ -1,7 +1,7 @@ import mockFs from "mock-fs"; import { build } from "./build"; -import { DEFAULT_CONFIG } from "../../config"; -import { convertToNativePaths } from "../../jest.helpers."; +import { DEFAULT_CONFIG } from "../../../config"; +import { convertToNativePaths } from "../../../jest.helpers."; jest.mock("child_process", () => ({ execSync: jest.fn(), diff --git a/src/cli/commands/build.ts b/src/cli/commands/build/build.ts similarity index 74% rename from src/cli/commands/build.ts rename to src/cli/commands/build/build.ts index 7a30a68a..268b963c 100644 --- a/src/cli/commands/build.ts +++ b/src/cli/commands/build/build.ts @@ -1,48 +1,15 @@ import path from "path"; import chalk from "chalk"; -import { Command } from "commander"; -import { DEFAULT_CONFIG } from "../../config"; -import { detectProjectFolders, generateConfiguration } from "../../core/frameworks"; +import { detectProjectFolders, generateConfiguration } from "../../../core/frameworks"; import { - configureOptions, findUpPackageJsonDir, - isUserOption, isUserOrConfigOption, logger, - matchLoadedConfigName, pathExists, readWorkflowFile, runCommand, swaCliConfigFilename, -} from "../../core/utils"; - -export default function registerCommand(program: Command) { - program - .command("build [configName|appLocation]") - .usage("[configName|appLocation] [options]") - .description("build your project") - .option("-a, --app-location ", "the folder containing the source code of the front-end application", DEFAULT_CONFIG.appLocation) - .option("-i, --api-location ", "the folder containing the source code of the API application", DEFAULT_CONFIG.apiLocation) - .option("-O, --output-location ", "the folder containing the built source of the front-end application", DEFAULT_CONFIG.outputLocation) - .option("-A, --app-build-command ", "the command used to build your app", DEFAULT_CONFIG.appBuildCommand) - .option("-I, --api-build-command ", "the command used to build your api", DEFAULT_CONFIG.apiBuildCommand) - .option("--auto", "automatically detect how to build your app and api", false) - .action(async (positionalArg: string | undefined, _options: SWACLIConfig, command: Command) => { - positionalArg = positionalArg?.trim(); - const options = await configureOptions(positionalArg, command.optsWithGlobals(), command, "build"); - if (positionalArg && !matchLoadedConfigName(positionalArg)) { - if (isUserOption("appLocation")) { - logger.error(`swa build cannot be when with --app-location option is also set.`); - logger.error(`You either have to use the positional argument or option, not both at the same time.`, true); - } - - // If it's not the config name, then it's the app location - options.appLocation = positionalArg; - } - - await build(options); - }); -} +} from "../../../core/utils"; export async function build(options: SWACLIConfig) { const workflowConfig = readWorkflowFile(); diff --git a/src/cli/commands/build/index.ts b/src/cli/commands/build/index.ts new file mode 100644 index 00000000..555ff323 --- /dev/null +++ b/src/cli/commands/build/index.ts @@ -0,0 +1,2 @@ +export * from "./build"; +export { default as registerBuild } from "./register"; diff --git a/src/cli/commands/build/register.ts b/src/cli/commands/build/register.ts new file mode 100644 index 00000000..270a9d17 --- /dev/null +++ b/src/cli/commands/build/register.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { DEFAULT_CONFIG } from "../../../config"; +import { configureOptions, isUserOption, logger, matchLoadedConfigName } from "../../../core/utils"; +import { build } from "./build"; + +export default function registerCommand(program: Command) { + program + .command("build [configName|appLocation]") + .usage("[configName|appLocation] [options]") + .description("build your project") + .option("-a, --app-location ", "the folder containing the source code of the front-end application", DEFAULT_CONFIG.appLocation) + .option("-i, --api-location ", "the folder containing the source code of the API application", DEFAULT_CONFIG.apiLocation) + .option("-O, --output-location ", "the folder containing the built source of the front-end application", DEFAULT_CONFIG.outputLocation) + .option("-A, --app-build-command ", "the command used to build your app", DEFAULT_CONFIG.appBuildCommand) + .option("-I, --api-build-command ", "the command used to build your api", DEFAULT_CONFIG.apiBuildCommand) + .option("--auto", "automatically detect how to build your app and api", false) + .action(async (positionalArg: string | undefined, _options: SWACLIConfig, command: Command) => { + positionalArg = positionalArg?.trim(); + const options = await configureOptions(positionalArg, command.optsWithGlobals(), command, "build"); + if (positionalArg && !matchLoadedConfigName(positionalArg)) { + if (isUserOption("appLocation")) { + logger.error(`swa build cannot be when with --app-location option is also set.`); + logger.error(`You either have to use the positional argument or option, not both at the same time.`, true); + } + + // If it's not the config name, then it's the app location + options.appLocation = positionalArg; + } + + await build(options); + }); +} diff --git a/src/cli/commands/deploy.spec.ts b/src/cli/commands/deploy/deploy.spec.ts similarity index 94% rename from src/cli/commands/deploy.spec.ts rename to src/cli/commands/deploy/deploy.spec.ts index 791b5f88..7d209bd4 100644 --- a/src/cli/commands/deploy.spec.ts +++ b/src/cli/commands/deploy/deploy.spec.ts @@ -1,19 +1,19 @@ import child_process from "child_process"; import mockFs from "mock-fs"; import path from "path"; -import { logger } from "../../core"; -import * as accountModule from "../../core/account"; -import * as deployClientModule from "../../core/deploy-client"; +import { logger } from "../../../core"; +import * as accountModule from "../../../core/account"; +import * as deployClientModule from "../../../core/deploy-client"; import { deploy } from "./deploy"; -import * as loginModule from "./login"; +import * as loginModule from "../login/login"; -const pkg = require(path.join(__dirname, "..", "..", "..", "package.json")); +const pkg = require(path.join(__dirname, "..", "..", "..", "..", "package.json")); jest.mock("ora", () => { return jest.fn(); }); -jest.mock("../../core/utils/logger", () => { +jest.mock("../../../core/utils/logger", () => { return { logger: { error: jest.fn(), diff --git a/src/cli/commands/deploy.ts b/src/cli/commands/deploy/deploy.ts similarity index 78% rename from src/cli/commands/deploy.ts rename to src/cli/commands/deploy/deploy.ts index eae20a66..11eca878 100644 --- a/src/cli/commands/deploy.ts +++ b/src/cli/commands/deploy/deploy.ts @@ -1,84 +1,22 @@ import chalk from "chalk"; import { spawn } from "child_process"; -import { Command } from "commander"; import fs from "fs"; import ora, { Ora } from "ora"; import path from "path"; -import { DEFAULT_CONFIG } from "../../config"; import { - configureOptions, findSWAConfigFile, getCurrentSwaCliConfigFromFile, - isUserOption, logger, logGiHubIssueMessageAndExit, - matchLoadedConfigName, readWorkflowFile, updateSwaCliConfigFile, -} from "../../core"; -import { chooseOrCreateProjectDetails, getStaticSiteDeployment } from "../../core/account"; -import { cleanUp, getDeployClientPath } from "../../core/deploy-client"; -import { swaCLIEnv } from "../../core/env"; -import { addSharedLoginOptionsToCommand, login } from "./login"; - -const packageInfo = require(path.join(__dirname, "..", "..", "..", "package.json")); - -export default function registerCommand(program: Command) { - const deployCommand = program - .command("deploy [configName|outputLocation]") - .usage("[configName|outputLocation] [options]") - .description("deploy the current project to Azure Static Web Apps") - .option("-a, --app-location ", "the folder containing the source code of the front-end application", DEFAULT_CONFIG.appLocation) - .option("-i, --api-location ", "the folder containing the source code of the API application", DEFAULT_CONFIG.apiLocation) - .option("-O, --output-location ", "the folder containing the built source of the front-end application", DEFAULT_CONFIG.outputLocation) - .option( - "-w, --swa-config-location ", - "the directory where the staticwebapp.config.json file is located", - DEFAULT_CONFIG.swaConfigLocation - ) - .option("-d, --deployment-token ", "the secret token used to authenticate with the Static Web Apps") - .option("-dr, --dry-run", "simulate a deploy process without actually running it", DEFAULT_CONFIG.dryRun) - .option("-pt, --print-token", "print the deployment token", false) - .option("--env [environment]", "the type of deployment environment where to deploy the project", DEFAULT_CONFIG.env) - .action(async (positionalArg: string | undefined, _options: SWACLIConfig, command: Command) => { - positionalArg = positionalArg?.trim(); - const options = await configureOptions(positionalArg, command.optsWithGlobals(), command, "deploy"); - if (positionalArg && !matchLoadedConfigName(positionalArg)) { - if (isUserOption("outputLocation")) { - logger.error(`swa deploy cannot be used when --output-location option is also set.`); - logger.error(`You either have to use the positional argument or option, not both at the same time.`, true); - } - - // If it's not the config name, then it's the output location - options.outputLocation = positionalArg; - } - - await deploy(options); - }) - .addHelpText( - "after", - ` -Examples: - - Deploy using a deployment token - swa deploy ./dist/ --api-location ./api/ --deployment-token +} from "../../../core"; +import { chooseOrCreateProjectDetails, getStaticSiteDeployment } from "../../../core/account"; +import { cleanUp, getDeployClientPath } from "../../../core/deploy-client"; +import { swaCLIEnv } from "../../../core/env"; +import { login } from "../login"; - Deploy using a deployment token from env - SWA_CLI_DEPLOYMENT_TOKEN=123 swa deploy ./dist/ --api-location ./api/ - - Deploy using swa-cli.config.json file - swa deploy - swa deploy myconfig - - Print the deployment token - swa deploy --print-token - - Deploy to a specific environment - swa deploy --env production - ` - ); - addSharedLoginOptionsToCommand(deployCommand); -} +const packageInfo = require(path.join(__dirname, "..", "..", "..", "..", "package.json")); export async function deploy(options: SWACLIConfig) { const { SWA_CLI_DEPLOYMENT_TOKEN, SWA_CLI_DEBUG } = swaCLIEnv(); diff --git a/src/cli/commands/deploy/index.ts b/src/cli/commands/deploy/index.ts new file mode 100644 index 00000000..9906b165 --- /dev/null +++ b/src/cli/commands/deploy/index.ts @@ -0,0 +1,2 @@ +export * from "./deploy"; +export { default as registerDeploy } from "./register"; diff --git a/src/cli/commands/deploy/register.ts b/src/cli/commands/deploy/register.ts new file mode 100644 index 00000000..c5cfd32e --- /dev/null +++ b/src/cli/commands/deploy/register.ts @@ -0,0 +1,62 @@ +import { Command } from "commander"; +import { DEFAULT_CONFIG } from "../../../config"; +import { configureOptions, isUserOption, logger, matchLoadedConfigName } from "../../../core"; +import { addSharedLoginOptionsToCommand } from "../login"; +import { deploy } from "./deploy"; + +export default function registerCommand(program: Command) { + const deployCommand = program + .command("deploy [configName|outputLocation]") + .usage("[configName|outputLocation] [options]") + .description("deploy the current project to Azure Static Web Apps") + .option("-a, --app-location ", "the folder containing the source code of the front-end application", DEFAULT_CONFIG.appLocation) + .option("-i, --api-location ", "the folder containing the source code of the API application", DEFAULT_CONFIG.apiLocation) + .option("-O, --output-location ", "the folder containing the built source of the front-end application", DEFAULT_CONFIG.outputLocation) + .option( + "-w, --swa-config-location ", + "the directory where the staticwebapp.config.json file is located", + DEFAULT_CONFIG.swaConfigLocation + ) + .option("-d, --deployment-token ", "the secret token used to authenticate with the Static Web Apps") + .option("-dr, --dry-run", "simulate a deploy process without actually running it", DEFAULT_CONFIG.dryRun) + .option("-pt, --print-token", "print the deployment token", false) + .option("--env [environment]", "the type of deployment environment where to deploy the project", DEFAULT_CONFIG.env) + .action(async (positionalArg: string | undefined, _options: SWACLIConfig, command: Command) => { + positionalArg = positionalArg?.trim(); + const options = await configureOptions(positionalArg, command.optsWithGlobals(), command, "deploy"); + if (positionalArg && !matchLoadedConfigName(positionalArg)) { + if (isUserOption("outputLocation")) { + logger.error(`swa deploy cannot be used when --output-location option is also set.`); + logger.error(`You either have to use the positional argument or option, not both at the same time.`, true); + } + + // If it's not the config name, then it's the output location + options.outputLocation = positionalArg; + } + + await deploy(options); + }) + .addHelpText( + "after", + ` +Examples: + + Deploy using a deployment token + swa deploy ./dist/ --api-location ./api/ --deployment-token + + Deploy using a deployment token from env + SWA_CLI_DEPLOYMENT_TOKEN=123 swa deploy ./dist/ --api-location ./api/ + + Deploy using swa-cli.config.json file + swa deploy + swa deploy myconfig + + Print the deployment token + swa deploy --print-token + + Deploy to a specific environment + swa deploy --env production + ` + ); + addSharedLoginOptionsToCommand(deployCommand); +} diff --git a/src/cli/commands/docs.ts b/src/cli/commands/docs.ts index 430d8447..c2d00020 100644 --- a/src/cli/commands/docs.ts +++ b/src/cli/commands/docs.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import open from "open"; -export default function registerDocs(program: Command) { +export function registerDocs(program: Command) { program .command("docs") .description("open Azure Static Web Apps CLI documentations") diff --git a/src/cli/commands/init/index.ts b/src/cli/commands/init/index.ts new file mode 100644 index 00000000..70e94c55 --- /dev/null +++ b/src/cli/commands/init/index.ts @@ -0,0 +1,2 @@ +export * from "./init"; +export { default as registerInit } from "./register"; diff --git a/src/cli/commands/init.spec.ts b/src/cli/commands/init/init.spec.ts similarity index 97% rename from src/cli/commands/init.spec.ts rename to src/cli/commands/init/init.spec.ts index c6fcb8a9..c9e140df 100644 --- a/src/cli/commands/init.spec.ts +++ b/src/cli/commands/init/init.spec.ts @@ -1,9 +1,9 @@ import fs from "fs"; import mockFs from "mock-fs"; import { init } from "./init"; -import { DEFAULT_CONFIG } from "../../config"; -import { swaCliConfigFilename } from "../../core/utils"; -import { convertToNativePaths, convertToUnixPaths } from "../../jest.helpers."; +import { DEFAULT_CONFIG } from "../../../config"; +import { swaCliConfigFilename } from "../../../core/utils"; +import { convertToNativePaths, convertToUnixPaths } from "../../../jest.helpers."; jest.mock("prompts", () => jest.fn()); diff --git a/src/cli/commands/init.ts b/src/cli/commands/init/init.ts similarity index 87% rename from src/cli/commands/init.ts rename to src/cli/commands/init/init.ts index be33efda..f5cfcee1 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init/init.ts @@ -1,40 +1,16 @@ import chalk from "chalk"; -import { Command } from "commander"; import path from "path"; import process from "process"; -import { promptOrUseDefault } from "../../core/prompts"; +import { promptOrUseDefault } from "../../../core/prompts"; import { - configureOptions, dasherize, hasConfigurationNameInConfigFile, - isUserOption, logger, swaCliConfigFileExists, swaCliConfigFilename, writeConfigFile, -} from "../../core/utils"; -import { detectProjectFolders, generateConfiguration, isDescendantPath } from "../../core/frameworks"; - -export default function registerCommand(program: Command) { - program - .command("init [configName]") - .usage("[configName] [options]") - .description("initialize a new static web app project") - .option("-y, --yes", "answer yes to all prompts (disable interactive mode)", false) - .action(async (configName: string | undefined, _options: SWACLIConfig, command: Command) => { - const options = await configureOptions(undefined, command.optsWithGlobals(), command, "init", false); - if (configName) { - if (isUserOption("configName")) { - logger.error(`swa init cannot be used when --config-name option is also set.`); - logger.error(`You either have to use the positional argument or option, not both at the same time.`, true); - } - - options.configName = configName; - } - - await init(options, !process.env.SWA_CLI_INTERNAL_COMMAND); - }); -} +} from "../../../core/utils"; +import { detectProjectFolders, generateConfiguration, isDescendantPath } from "../../../core/frameworks"; export async function init(options: SWACLIConfig, showHints: boolean = true) { const configFilePath = options.config!; diff --git a/src/cli/commands/init/register.ts b/src/cli/commands/init/register.ts new file mode 100644 index 00000000..729b0336 --- /dev/null +++ b/src/cli/commands/init/register.ts @@ -0,0 +1,25 @@ +import { Command } from "commander"; +import process from "process"; +import { configureOptions, isUserOption, logger } from "../../../core/utils"; +import { init } from "./init"; + +export default function registerCommand(program: Command) { + program + .command("init [configName]") + .usage("[configName] [options]") + .description("initialize a new static web app project") + .option("-y, --yes", "answer yes to all prompts (disable interactive mode)", false) + .action(async (configName: string | undefined, _options: SWACLIConfig, command: Command) => { + const options = await configureOptions(undefined, command.optsWithGlobals(), command, "init", false); + if (configName) { + if (isUserOption("configName")) { + logger.error(`swa init cannot be used when --config-name option is also set.`); + logger.error(`You either have to use the positional argument or option, not both at the same time.`, true); + } + + options.configName = configName; + } + + await init(options, !process.env.SWA_CLI_INTERNAL_COMMAND); + }); +} diff --git a/src/cli/commands/login/index.ts b/src/cli/commands/login/index.ts new file mode 100644 index 00000000..efe93679 --- /dev/null +++ b/src/cli/commands/login/index.ts @@ -0,0 +1,2 @@ +export * from "./login"; +export { default as registerLogin, addSharedLoginOptionsToCommand } from "./register"; diff --git a/src/cli/commands/login.ts b/src/cli/commands/login/login.ts similarity index 60% rename from src/cli/commands/login.ts rename to src/cli/commands/login/login.ts index f98db424..2182b551 100644 --- a/src/cli/commands/login.ts +++ b/src/cli/commands/login/login.ts @@ -1,83 +1,32 @@ import { TokenCredential } from "@azure/identity"; import chalk from "chalk"; -import { Command } from "commander"; import dotenv from "dotenv"; import { existsSync, promises as fsPromises } from "fs"; import path from "path"; -import { DEFAULT_CONFIG } from "../../config"; -import { configureOptions, logger, logGiHubIssueMessageAndExit } from "../../core"; -import { authenticateWithAzureIdentity, listSubscriptions, listTenants } from "../../core/account"; -import { ENV_FILENAME } from "../../core/constants"; -import { updateGitIgnore } from "../../core/git"; -import { chooseSubscription, chooseTenant } from "../../core/prompts"; -import { Environment } from "../../core/swa-cli-persistence-plugin/impl/azure-environment"; +import { logger, logGiHubIssueMessageAndExit } from "../../../core"; +import { authenticateWithAzureIdentity, listSubscriptions, listTenants } from "../../../core/account"; +import { ENV_FILENAME } from "../../../core/constants"; +import { updateGitIgnore } from "../../../core/git"; +import { chooseSubscription, chooseTenant } from "../../../core/prompts"; +import { Environment } from "../../../core/swa-cli-persistence-plugin/impl/azure-environment"; const { readFile, writeFile } = fsPromises; const defaultScope = `${Environment.AzureCloud.resourceManagerEndpointUrl}/.default`; -export function addSharedLoginOptionsToCommand(command: Command) { - command - .option("-S, --subscription-id ", "Azure subscription ID used by this project", DEFAULT_CONFIG.subscriptionId) - .option("-R, --resource-group ", "Azure resource group used by this project", DEFAULT_CONFIG.resourceGroup) - .option("-T, --tenant-id ", "Azure tenant ID", DEFAULT_CONFIG.tenantId) - .option("-C, --client-id ", "Azure client ID", DEFAULT_CONFIG.clientId) - .option("-CS, --client-secret ", "Azure client secret", DEFAULT_CONFIG.clientSecret) - .option("-n, --app-name ", "Azure Static Web App application name", DEFAULT_CONFIG.appName) - .option("-CC, --clear-credentials", "clear persisted credentials before login", DEFAULT_CONFIG.clearCredentials) - - .option("-u, --use-keychain", "enable using the operating system native keychain for persistent credentials", DEFAULT_CONFIG.useKeychain) - // Note: Commander does not automatically recognize the --no-* option, so we have to explicitly use --no-use-keychain- instead - .option("-nu, --no-use-keychain", "disable using the operating system native keychain", !DEFAULT_CONFIG.useKeychain); -} +export async function loginCommand(options: SWACLIConfig) { + try { + const { credentialChain, subscriptionId } = await login(options); -export default function registerCommand(program: Command) { - const loginCommand = program - .command("login") - .usage("[options]") - .description("login into Azure") - .action(async (_options: SWACLIConfig, command: Command) => { - const options = await configureOptions(undefined, command.optsWithGlobals(), command, "login"); - - try { - const { credentialChain, subscriptionId } = await login(options); - - if (credentialChain && subscriptionId) { - logger.log(chalk.green(`✔ Successfully setup project!`)); - } else { - logger.log(chalk.red(`✘ Failed to setup project!`)); - logGiHubIssueMessageAndExit(); - } - } catch (error) { - logger.error(`Failed to setup project: ${(error as any).message}`); - logGiHubIssueMessageAndExit(); - } - }) - .addHelpText( - "after", - ` -Examples: - - Interactive login - swa login - - Interactive login without persisting credentials - swa login --no-use-keychain - - Log in into specific tenant - swa login --tenant-id 00000000-0000-0000-0000-000000000000 - - Log in using a specific subscription, resource group or an application - swa login --subscription-id my-subscription \\ - --resource-group my-resource-group \\ - --app-name my-static-site - - Login using service principal - swa login --tenant-id 00000000-0000-0000-0000-000000000000 \\ - --client-id 00000000-0000-0000-0000-000000000000 \\ - --client-secret 0000000000000000000000000000000000000000000000000000000000000000 - ` - ); - addSharedLoginOptionsToCommand(loginCommand); + if (credentialChain && subscriptionId) { + logger.log(chalk.green(`✔ Successfully setup project!`)); + } else { + logger.log(chalk.red(`✘ Failed to setup project!`)); + logGiHubIssueMessageAndExit(); + } + } catch (error) { + logger.error(`Failed to setup project: ${(error as any).message}`); + logGiHubIssueMessageAndExit(); + } } export async function login(options: SWACLIConfig): Promise { diff --git a/src/cli/commands/login/register.ts b/src/cli/commands/login/register.ts new file mode 100644 index 00000000..e0a55891 --- /dev/null +++ b/src/cli/commands/login/register.ts @@ -0,0 +1,56 @@ +import { Command } from "commander"; +import { DEFAULT_CONFIG } from "../../../config"; +import { configureOptions } from "../../../core"; +import { loginCommand as login } from "./login"; + +export function addSharedLoginOptionsToCommand(command: Command) { + command + .option("-S, --subscription-id ", "Azure subscription ID used by this project", DEFAULT_CONFIG.subscriptionId) + .option("-R, --resource-group ", "Azure resource group used by this project", DEFAULT_CONFIG.resourceGroup) + .option("-T, --tenant-id ", "Azure tenant ID", DEFAULT_CONFIG.tenantId) + .option("-C, --client-id ", "Azure client ID", DEFAULT_CONFIG.clientId) + .option("-CS, --client-secret ", "Azure client secret", DEFAULT_CONFIG.clientSecret) + .option("-n, --app-name ", "Azure Static Web App application name", DEFAULT_CONFIG.appName) + .option("-CC, --clear-credentials", "clear persisted credentials before login", DEFAULT_CONFIG.clearCredentials) + + .option("-u, --use-keychain", "enable using the operating system native keychain for persistent credentials", DEFAULT_CONFIG.useKeychain) + // Note: Commander does not automatically recognize the --no-* option, so we have to explicitly use --no-use-keychain- instead + .option("-nu, --no-use-keychain", "disable using the operating system native keychain", !DEFAULT_CONFIG.useKeychain); +} + +export default function registerCommand(program: Command) { + const loginCommand = program + .command("login") + .usage("[options]") + .description("login into Azure") + .action(async (_options: SWACLIConfig, command: Command) => { + const options = await configureOptions(undefined, command.optsWithGlobals(), command, "login"); + await login(options); + }) + .addHelpText( + "after", + ` +Examples: + + Interactive login + swa login + + Interactive login without persisting credentials + swa login --no-use-keychain + + Log in into specific tenant + swa login --tenant-id 00000000-0000-0000-0000-000000000000 + + Log in using a specific subscription, resource group or an application + swa login --subscription-id my-subscription \\ + --resource-group my-resource-group \\ + --app-name my-static-site + + Login using service principal + swa login --tenant-id 00000000-0000-0000-0000-000000000000 \\ + --client-id 00000000-0000-0000-0000-000000000000 \\ + --client-secret 0000000000000000000000000000000000000000000000000000000000000000 + ` + ); + addSharedLoginOptionsToCommand(loginCommand); +} diff --git a/src/cli/commands/start/index.ts b/src/cli/commands/start/index.ts new file mode 100644 index 00000000..d81cd027 --- /dev/null +++ b/src/cli/commands/start/index.ts @@ -0,0 +1,2 @@ +export * from "./start"; +export { default as registerStart } from "./register"; diff --git a/src/cli/commands/start/register.ts b/src/cli/commands/start/register.ts new file mode 100644 index 00000000..fb92dc6d --- /dev/null +++ b/src/cli/commands/start/register.ts @@ -0,0 +1,95 @@ +import chalk from "chalk"; +import { Command } from "commander"; +import { DEFAULT_CONFIG } from "../../../config"; +import { configureOptions, isHttpUrl, isUserOption, logger, matchLoadedConfigName, parseServerTimeout, parsePort } from "../../../core"; +import { start } from "./start"; + +export default function registerCommand(program: Command) { + program + .command("start [configName|outputLocation|appDevserverUrl]") + .usage("[configName|outputLocation|appDevserverUrl] [options]") + .description("start the emulator from a directory or bind to a dev server") + .option("-a, --app-location ", "the folder containing the source code of the front-end application", DEFAULT_CONFIG.appLocation) + .option("-i, --api-location ", "the folder containing the source code of the API application", DEFAULT_CONFIG.apiLocation) + .option("-O, --output-location ", "the folder containing the built source of the front-end application", DEFAULT_CONFIG.outputLocation) + .option( + "-D, --app-devserver-url ", + "connect to the app dev server at this URL instead of using output location", + DEFAULT_CONFIG.appDevserverUrl + ) + .option("-is, --api-devserver-url ", "connect to the api server at this URL instead of using api location", DEFAULT_CONFIG.apiDevserverUrl) + .option("-j, --api-port ", "the API server port passed to `func start`", parsePort, DEFAULT_CONFIG.apiPort) + .option("-q, --host ", "the host address to use for the CLI dev server", DEFAULT_CONFIG.host) + .option("-p, --port ", "the port value to use for the CLI dev server", parsePort, DEFAULT_CONFIG.port) + + .option("-s, --ssl", "serve the front-end application and API over HTTPS", DEFAULT_CONFIG.ssl) + .option("-e, --ssl-cert ", "the SSL certificate (.crt) to use when enabling HTTPS", DEFAULT_CONFIG.sslCert) + .option("-k, --ssl-key ", "the SSL key (.key) to use when enabling HTTPS", DEFAULT_CONFIG.sslKey) + .option("-r, --run ", "run a custom shell command or script file at startup", DEFAULT_CONFIG.run) + .option( + "-t, --devserver-timeout