import { concurrently } from "concurrently"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { DEFAULT_CONFIG } from "../../../config.js"; import { askNewPort, isAcceptingTcpConnections, parseUrl } from "../../../core/utils/net.js"; import { logger } from "../../../core/utils/logger.js"; import { createStartupScriptCommand } from "../../../core/utils/cli.js"; import { readWorkflowFile } from "../../../core/utils/workflow-config.js"; import { getNodeMajorVersion, isCoreToolsVersionCompatible, getCoreToolsBinary, detectTargetCoreToolsVersion, } from "../../../core/func-core-tools.js"; import { DATA_API_BUILDER_BINARY_NAME, DATA_API_BUILDER_DEFAULT_CONFIG_FILE_NAME } from "../../../core/constants.js"; import { getDataApiBuilderBinaryPath } from "../../../core/dataApiBuilder/index.js"; import { swaCLIEnv } from "../../../core/env.js"; import { getCertificate } from "../../../core/ssl.js"; import { loadPackageJson } from "../../../core/utils/json.js"; const packageInfo = loadPackageJson(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // This is the path to ./dist/msha/server.js const mshaPath = path.resolve(__dirname, "../../../../dist/msha/server.js"); // import { createRequire } from "node:module"; // const require = createRequire(import.meta.url); // const mshaPath = require.resolve("../../../msha/server.js"); export async function start(options) { // WARNING: // environment variables are populated using values provided by the user to the CLI. // Code below doesn't have access to these environment variables which are defined later below. // Make sure this code (or code from utils) does't depend on environment variables! let { appLocation, apiLocation, dataApiLocation, outputLocation, appDevserverUrl, apiDevserverUrl, dataApiDevserverUrl, apiPort, dataApiPort, devserverTimeout, ssl, sslCert, sslKey, host, port, run, open, funcArgs, swaConfigLocation, verbose, } = options; let useApiDevServer = undefined; let useDataApiDevServer = undefined; let startupCommand = undefined; let resolvedPortNumber = await isAcceptingTcpConnections({ host, port }); if (resolvedPortNumber === 0) { logger.warn(`Port ${port} is already taken!`); resolvedPortNumber = await askNewPort(); } else { logger.silly(`Port ${port} is available. Use it.`); } // still no luck or user refused to use a random port if (resolvedPortNumber === 0) { logger.error(`Port ${port} is already in use. Use '--port' to specify a different port.`, true); } // set the new port number in case we picked a new one (see net.isAcceptingTcpConnections()) logger.silly(`Resolved port number: ${resolvedPortNumber}`); port = resolvedPortNumber; // resolve the absolute path to the appLocation appLocation = path.resolve(appLocation); if (appDevserverUrl) { logger.silly(`appDevserverUrl provided, we will try connect to dev server at ${outputLocation}`); // TODO: properly refactor this after GA to send appDevserverUrl to the server outputLocation = appDevserverUrl; } else { logger.silly(`Resolving outputLocation=${outputLocation} full path...`); let resolvedOutputLocation = path.resolve(appLocation, outputLocation); // if folder exists, start the emulator from a specific build folder (outputLocation), relative to appLocation if (fs.existsSync(resolvedOutputLocation)) { outputLocation = resolvedOutputLocation; } // check for build folder (outputLocation) using the absolute location else if (!fs.existsSync(outputLocation)) { logger.error(`The folder "${resolvedOutputLocation}" is not found. Exit.`, true); return; } logger.silly(`Resolved outputLocation:`); logger.silly(` ${outputLocation}`); } if (apiDevserverUrl) { // TODO: properly refactor this after GA to send apiDevserverUrl to the server useApiDevServer = apiDevserverUrl; apiLocation = apiDevserverUrl; logger.silly(`Api Dev Server found: ${apiDevserverUrl}`); } else if (apiLocation) { // resolves to the absolute path of the apiLocation const resolvedApiLocation = path.resolve(apiLocation); // make sure api folder exists if (fs.existsSync(resolvedApiLocation)) { apiLocation = resolvedApiLocation; logger.silly(`Api Folder found: ${apiLocation}`); } else { logger.info(`Skipping Api because folder "${resolvedApiLocation}" is missing`, "swa"); } } if (dataApiDevserverUrl) { useDataApiDevServer = dataApiDevserverUrl; dataApiLocation = dataApiDevserverUrl; logger.silly(`Data Api Dev Server found: ${dataApiDevserverUrl}`); } else if (dataApiLocation) { const resolvedDataApiLocation = path.resolve(dataApiLocation); if (fs.existsSync(resolvedDataApiLocation)) { dataApiLocation = resolvedDataApiLocation; logger.silly(`Data Api Folder found: ${dataApiLocation}`); } else { logger.info(`Skipping Data Api because folder "${resolvedDataApiLocation}" is missing`, "swa"); } } let userWorkflowConfig = { appLocation, outputLocation, apiLocation, }; // mix CLI args with the project's build workflow configuration (if any) // use any specific workflow config that the user might provide undef ".github/workflows/" // Note: CLI args will take precedence over workflow config try { // TODO: not sure if we should still do this here, as config/user options should override // over any options in the workflow config, but it seems to do the opposite here. userWorkflowConfig = readWorkflowFile({ userWorkflowConfig, }); logger.silly(`User workflow config:`); logger.silly(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.`); } const isApiLocationExistsOnDisk = fs.existsSync(userWorkflowConfig?.apiLocation); // handle the API location config let serveApiCommand = "echo 'No API found. Skipping'"; if (useApiDevServer) { serveApiCommand = `echo 'using API dev server at ${useApiDevServer}'`; // get the API port from the dev server apiPort = parseUrl(useApiDevServer)?.port; } else { if (apiLocation && userWorkflowConfig?.apiLocation) { // check if the func binary is globally available and if not, download it const funcBinary = await getCoreToolsBinary(); const nodeMajorVersion = getNodeMajorVersion(); const targetVersion = detectTargetCoreToolsVersion(nodeMajorVersion); if (!funcBinary) { // prettier-ignore logger.error(`\nCould not find or install Azure Functions Core Tools.\n` + `Install Azure Functions Core Tools with:\n\n` + ` npm i -g azure-functions-core-tools@${targetVersion} --unsafe-perm true\n\n` + `See https://aka.ms/functions-core-tools for more information.`, true); } else { if (isCoreToolsVersionCompatible(targetVersion, nodeMajorVersion) === false) { logger.error(`Found Azure Functions Core Tools v${targetVersion} which is incompatible with your current Node.js v${process.versions.node}.`); logger.error("See https://aka.ms/functions-node-versions for more information."); process.exit(1); } // serve the api if and only if the user provides a folder via the --api-location flag if (isApiLocationExistsOnDisk) { serveApiCommand = `cd "${userWorkflowConfig.apiLocation}" && "${funcBinary}" start --cors "*" --port ${apiPort} ${funcArgs ?? ""}`; } } } } let serveDataApiCommand = "echo 'No Data API found'. Skipping"; let startDataApiBuilderNeeded = false; if (useDataApiDevServer) { serveDataApiCommand = `echo using Data API server at ${useDataApiDevServer}`; dataApiPort = parseUrl(useDataApiDevServer)?.port; } else { if (dataApiLocation) { const dataApiBinary = await getDataApiBuilderBinaryPath(); if (!dataApiBinary) { logger.error(`Could not find or install ${DATA_API_BUILDER_BINARY_NAME} binary. If you already have data-api-builder installed, try connecting using --data-api-devserver-url by starting data-api-builder engine separately. Exiting!!`, true); } else { serveDataApiCommand = `cd "${dataApiLocation}" && "${dataApiBinary}" start -c ${DATA_API_BUILDER_DEFAULT_CONFIG_FILE_NAME} --no-https-redirect`; dataApiPort = DEFAULT_CONFIG.dataApiPort; startDataApiBuilderNeeded = true; } } logger.silly(`Running ${serveDataApiCommand}`); } if (ssl) { if (sslCert === undefined && sslKey === undefined) { logger.warn(`WARNING: Using built-in UNSIGNED certificate. DO NOT USE IN PRODUCTION!`); const pemFilepath = await getCertificate({ selfSigned: true, days: 365, commonName: host, organization: `Azure Static Web Apps CLI ${packageInfo.version}`, organizationUnit: "Azure Engineering", emailAddress: `secure@microsoft.com`, }); sslCert = pemFilepath; sslKey = pemFilepath; } else { // user provided cert and key, so we'll use them sslCert = sslCert && path.resolve(sslCert); sslKey = sslKey && path.resolve(sslKey); } } if (run) { startupCommand = createStartupScriptCommand(run, options); } // resolve the following config to their absolute paths // note: the server will perform a search starting from this path swaConfigLocation = path.resolve(swaConfigLocation || userWorkflowConfig?.appLocation || process.cwd()); // WARNING: code from above doesn't have access to env vars which are only defined below // set env vars for current command const envVarsObj = { SWA_RUNTIME_CONFIG_LOCATION: swaConfigLocation, SWA_RUNTIME_WORKFLOW_LOCATION: userWorkflowConfig?.files?.[0], SWA_CLI_DEBUG: verbose, SWA_CLI_API_PORT: `${apiPort}`, SWA_CLI_APP_LOCATION: userWorkflowConfig?.appLocation, SWA_CLI_OUTPUT_LOCATION: userWorkflowConfig?.outputLocation, SWA_CLI_API_LOCATION: userWorkflowConfig?.apiLocation, SWA_CLI_DATA_API_LOCATION: dataApiLocation, SWA_CLI_DATA_API_PORT: `${dataApiPort}`, SWA_CLI_HOST: `${host}`, SWA_CLI_PORT: `${port}`, SWA_CLI_APP_SSL: ssl ? "true" : "false", SWA_CLI_APP_SSL_CERT: sslCert, SWA_CLI_APP_SSL_KEY: sslKey, SWA_CLI_STARTUP_COMMAND: startupCommand, SWA_CLI_VERSION: packageInfo.version, SWA_CLI_SERVER_TIMEOUT: `${devserverTimeout}`, SWA_CLI_OPEN_BROWSER: open ? "true" : "false", }; // merge SWA CLI env variables with process.env process.env = { ...swaCLIEnv(envVarsObj), // Prevent react-scripts from opening browser BROWSER: "none", }; // INFO: from here, code may access SWA CLI env vars. const swa_cli_env = swaCLIEnv(); // Convert the swa_cli_env to a Record object so that type checking // works in the ConcurrentlyCommandInput[] array let env = {}; for (const [k, v] of Object.entries(swa_cli_env)) { env[k] = v; } const concurrentlyCommands = [ // start the reverse proxy { command: `node "${mshaPath}"`, name: "swa", env, prefixColor: "gray.dim" }, ]; if (isApiLocationExistsOnDisk) { concurrentlyCommands.push( // serve the api, if it's available { command: serveApiCommand, name: "api", env, prefixColor: "gray.dim" }); } if (startDataApiBuilderNeeded) { concurrentlyCommands.push({ command: serveDataApiCommand, name: "dataApi", env }); } // run an external script, if it's available if (startupCommand) { let startupPath = userWorkflowConfig?.appLocation; concurrentlyCommands.push({ command: `cd "${startupPath}" && ${startupCommand}`, name: "run", env, prefixColor: "gray.dim" }); } logger.silly(`Starting the SWA emulator with the following configuration:`); logger.silly({ ssl: [ssl, sslCert, sslKey], env: envVarsObj, commands: { swa: concurrentlyCommands.find((c) => c.name === "swa")?.command, api: concurrentlyCommands.find((c) => c.name === "api")?.command, dataApi: concurrentlyCommands.find((c) => c.name == "dataApi")?.command, run: concurrentlyCommands.find((c) => c.name === "run")?.command, }, }); const concurrentlyOptions = { restartTries: 0, killOthers: ["failure", "success"], raw: true }; const { result } = concurrently(concurrentlyCommands, concurrentlyOptions); await result .then((errorEvent) => { const killedCommand = errorEvent.filter((event) => event.killed).pop(); const exitCode = killedCommand?.exitCode; logger.silly(`SWA emulator exited with code ${exitCode}`); process.exit(); }, (errorEvent) => { const killedCommand = errorEvent.filter((event) => event.killed).pop(); const commandName = killedCommand?.command.name; const exitCode = killedCommand?.exitCode; let commandMessage = ``; switch (commandName) { case "swa": commandMessage = `SWA emulator exited with code ${exitCode}`; break; case "api": commandMessage = `API server exited with code ${exitCode}`; break; case "dataApi": commandMessage = `Data API server exited with code ${exitCode}`; break; case "run": commandMessage = `the --run command exited with code ${exitCode}`; break; } logger.error(`SWA emulator stopped because ${commandMessage}.`, true); }) .catch((err) => { logger.error(err.message, true); }); } //# sourceMappingURL=start.js.map