305 lines
No EOL
15 KiB
JavaScript
305 lines
No EOL
15 KiB
JavaScript
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<string, unknown> 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
|