feat: Support running external commands on startup (#145)
* feat: Support running external commands on startup Fixes #144 * fix: make tests pass on Windows
This commit is contained in:
Родитель
d59a7decbd
Коммит
3c88860e8a
23
readme.md
23
readme.md
|
@ -118,17 +118,18 @@ swa start http://<APP_DEV_SERVER> --api=http://<API_DEV_SERVER>
|
|||
|
||||
If you need to override the default values, provide the following options:
|
||||
|
||||
| Options | Description | Default | Example |
|
||||
| -------------------------------- | --------------------------------------------------- | --------- | ---------------------------------------------------- |
|
||||
| `--app-location` | set location for the static app source code | `./` | `--app-location="./my-project"` |
|
||||
| `--app, --app-artifact-location` | set app artifact (dist) folder or dev server | `./` | `--app="./my-dist"` or `--app=http://localhost:4200` |
|
||||
| `--api, --api-artifact-location` | set the API folder or dev server | | `--api="./api"` or `--api=http://localhost:8083` |
|
||||
| `--api-port` | set the API server port | `7071` | `--api-port=8082` |
|
||||
| `--host` | set the emulator host address | `0.0.0.0` | `--host=192.168.68.80` |
|
||||
| `--port` | set the emulator port value | `4280` | `--port=8080` |
|
||||
| `--ssl` | serving the app and API over HTTPS (default: false) | `false` | `--ssl` or `--ssl=true` |
|
||||
| `--ssl-cert` | SSL certificate to use for serving HTTPS | | `--ssl-cert="/home/user/ssl/example.crt"` |
|
||||
| `--ssl-key` | SSL key to use for serving HTTPS | | `--ssl-key="/home/user/ssl/example.key"` |
|
||||
| Options | Description | Default | Example |
|
||||
| -------------------------------- | ---------------------------------------------------- | --------- | ---------------------------------------------------- |
|
||||
| `--app-location` | set location for the static app source code | `./` | `--app-location="./my-project"` |
|
||||
| `--app, --app-artifact-location` | set app artifact (dist) folder or dev server | `./` | `--app="./my-dist"` or `--app=http://localhost:4200` |
|
||||
| `--api, --api-artifact-location` | set the API folder or dev server | | `--api="./api"` or `--api=http://localhost:8083` |
|
||||
| `--api-port` | set the API server port | `7071` | `--api-port=8082` |
|
||||
| `--host` | set the emulator host address | `0.0.0.0` | `--host=192.168.68.80` |
|
||||
| `--port` | set the emulator port value | `4280` | `--port=8080` |
|
||||
| `--ssl` | serving the app and API over HTTPS (default: false) | `false` | `--ssl` or `--ssl=true` |
|
||||
| `--ssl-cert` | SSL certificate to use for serving HTTPS | | `--ssl-cert="/home/user/ssl/example.crt"` |
|
||||
| `--ssl-key` | SSL key to use for serving HTTPS | | `--ssl-key="/home/user/ssl/example.key"` |
|
||||
| `--run` | run a external program or npm/yarn script on startup | | `--run="npm:start"` or `--run="script.sh"` |
|
||||
|
||||
## Local authentication & authorization emulation
|
||||
|
||||
|
|
|
@ -3,11 +3,23 @@ import fs from "fs";
|
|||
import path from "path";
|
||||
import { DEFAULT_CONFIG } from "../../config";
|
||||
import builder from "../../core/builder";
|
||||
import { isAcceptingTcpConnections, isHttpUrl, logger, parseUrl, readWorkflowFile, validateDevServerConfig } from "../../core/utils";
|
||||
import {
|
||||
createStartupScriptCommand,
|
||||
isAcceptingTcpConnections,
|
||||
isHttpUrl,
|
||||
logger,
|
||||
parseUrl,
|
||||
readWorkflowFile,
|
||||
validateDevServerConfig,
|
||||
} from "../../core/utils";
|
||||
|
||||
export async function start(startContext: string, options: SWACLIConfig) {
|
||||
let useAppDevServer = undefined;
|
||||
let useApiDevServer = undefined;
|
||||
// WARNING: code below doesn't have access to SWA CLI env vars which are defined later below
|
||||
// make sure this code (or code from utils) does't depend on SWA CLI env vars!
|
||||
|
||||
let useAppDevServer: string | undefined | null = undefined;
|
||||
let useApiDevServer: string | undefined | null = undefined;
|
||||
let startupCommand: string | undefined | null = undefined;
|
||||
|
||||
if (isHttpUrl(startContext)) {
|
||||
useAppDevServer = await validateDevServerConfig(startContext);
|
||||
|
@ -59,7 +71,6 @@ export async function start(startContext: string, options: SWACLIConfig) {
|
|||
});
|
||||
|
||||
const isApiLocationExistsOnDisk = fs.existsSync(userConfig?.apiLocation!);
|
||||
// parse the API URI port
|
||||
|
||||
// handle the API location config
|
||||
let serveApiCommand = "echo No API found. Skipping";
|
||||
|
@ -86,6 +97,12 @@ export async function start(startContext: string, options: SWACLIConfig) {
|
|||
}
|
||||
}
|
||||
|
||||
if (options.run) {
|
||||
startupCommand = createStartupScriptCommand(options.run, options);
|
||||
}
|
||||
|
||||
// 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_CLI_DEBUG: options.verbose,
|
||||
|
@ -99,13 +116,16 @@ export async function start(startContext: string, options: SWACLIConfig) {
|
|||
SWA_CLI_APP_SSL: `${options.ssl}`,
|
||||
SWA_CLI_APP_SSL_CERT: options.sslCert,
|
||||
SWA_CLI_APP_SSL_KEY: options.sslKey,
|
||||
SWA_CLI_STARTUP_COMMAND: startupCommand as string,
|
||||
};
|
||||
|
||||
// merge SWA env variables with process.env
|
||||
process.env = { ...process.env, ...envVarsObj };
|
||||
|
||||
// INFO: from here code may access SWA CLI env vars.
|
||||
|
||||
const { env } = process;
|
||||
const concurrentlyCommands = [
|
||||
const concurrentlyCommands: concurrently.CommandObj[] = [
|
||||
// start the reverse proxy
|
||||
{ command: `node ${path.join(__dirname, "..", "..", "proxy", "server.js")}`, name: "swa", env, prefixColor: "gray.dim" },
|
||||
];
|
||||
|
@ -117,6 +137,13 @@ export async function start(startContext: string, options: SWACLIConfig) {
|
|||
);
|
||||
}
|
||||
|
||||
if (startupCommand) {
|
||||
concurrentlyCommands.push(
|
||||
// run an external script, if it's available
|
||||
{ command: `cd ${userConfig?.appLocation} && ${startupCommand}`, name: "run", env, prefixColor: "gray.dim" }
|
||||
);
|
||||
}
|
||||
|
||||
if (options.build) {
|
||||
// run the app/api builds
|
||||
await builder({
|
||||
|
@ -129,8 +156,9 @@ export async function start(startContext: string, options: SWACLIConfig) {
|
|||
ssl: [options.ssl, options.sslCert, options.sslKey],
|
||||
env: envVarsObj,
|
||||
commands: {
|
||||
app: concurrentlyCommands[0].command,
|
||||
api: concurrentlyCommands?.[1]?.command,
|
||||
swa: concurrentlyCommands.find((c) => c.name === "swa")?.command,
|
||||
api: concurrentlyCommands.find((c) => c.name === "api")?.command,
|
||||
run: concurrentlyCommands.find((c) => c.name === "run")?.command,
|
||||
},
|
||||
},
|
||||
"swa"
|
||||
|
|
|
@ -37,7 +37,10 @@ import { start } from "./commands/start";
|
|||
.option("--ssl", "serving the app and API over HTTPS", DEFAULT_CONFIG.ssl)
|
||||
.option("--ssl-cert <sslCertLocation>", "SSL certificate to use for serving HTTPS", DEFAULT_CONFIG.sslCert)
|
||||
.option("--ssl-key <sslKeyLocation>", "SSL key to use for serving HTTPS", DEFAULT_CONFIG.sslKey)
|
||||
.action(async (context: string = `.${path.sep}`, options: any) => {
|
||||
|
||||
.option("--run <startupScript>", "run a external program or npm/yarn script on startup", DEFAULT_CONFIG.run)
|
||||
|
||||
.action(async (context: string = `.${path.sep}`, options: SWACLIConfig) => {
|
||||
options = {
|
||||
...options,
|
||||
verbose: cli.verbose,
|
||||
|
|
|
@ -13,4 +13,5 @@ export const DEFAULT_CONFIG: SWACLIConfig = {
|
|||
apiBuildCommand: "npm run build --if-present",
|
||||
swaConfigFilename: "staticwebapp.config.json",
|
||||
swaConfigFilenameLegacy: "routes.json",
|
||||
run: undefined,
|
||||
};
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
import mockFs from "mock-fs";
|
||||
import path from "path";
|
||||
import { address, argv, findSWAConfigFile, logger, parsePort, readWorkflowFile, response, traverseFolder, validateCookie } from "./utils";
|
||||
import {
|
||||
address,
|
||||
argv,
|
||||
createStartupScriptCommand,
|
||||
findSWAConfigFile,
|
||||
logger,
|
||||
parsePort,
|
||||
readWorkflowFile,
|
||||
response,
|
||||
traverseFolder,
|
||||
validateCookie,
|
||||
} from "./utils";
|
||||
|
||||
describe("Utils", () => {
|
||||
const mockLoggerError = jest.spyOn(logger, "error").mockImplementation(() => {
|
||||
|
@ -776,4 +787,131 @@ jobs:
|
|||
expect(address("127.0.0.1", "4200", "https")).toBe("https://127.0.0.1:4200");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createStartupScriptCommand()", () => {
|
||||
describe("npm", () => {
|
||||
it("should parse npm scripts (simple)", () => {
|
||||
const cmd = createStartupScriptCommand("npm:start", {});
|
||||
expect(cmd).toBe("npm run start --if-present");
|
||||
});
|
||||
it("should parse npm scripts (with -)", () => {
|
||||
const cmd = createStartupScriptCommand("npm:start-foo", {});
|
||||
expect(cmd).toBe("npm run start-foo --if-present");
|
||||
});
|
||||
it("should parse npm scripts (with :)", () => {
|
||||
const cmd = createStartupScriptCommand("npm:start:foo", {});
|
||||
expect(cmd).toBe("npm run start:foo --if-present");
|
||||
});
|
||||
it("should parse npm scripts (with #)", () => {
|
||||
const cmd = createStartupScriptCommand("npm:start#foo", {});
|
||||
expect(cmd).toBe("npm run start#foo --if-present");
|
||||
});
|
||||
});
|
||||
describe("yarn", () => {
|
||||
it("should parse yarn scripts (simple)", () => {
|
||||
const cmd = createStartupScriptCommand("yarn:start", {});
|
||||
expect(cmd).toBe("yarn run start --if-present");
|
||||
});
|
||||
it("should parse yarn scripts (with -)", () => {
|
||||
const cmd = createStartupScriptCommand("yarn:start-foo", {});
|
||||
expect(cmd).toBe("yarn run start-foo --if-present");
|
||||
});
|
||||
it("should parse yarn scripts (with :)", () => {
|
||||
const cmd = createStartupScriptCommand("yarn:start:foo", {});
|
||||
expect(cmd).toBe("yarn run start:foo --if-present");
|
||||
});
|
||||
it("should parse yarn scripts (with #)", () => {
|
||||
const cmd = createStartupScriptCommand("yarn:start#foo", {});
|
||||
expect(cmd).toBe("yarn run start#foo --if-present");
|
||||
});
|
||||
});
|
||||
describe("npx", () => {
|
||||
it("should parse npx command (simple)", () => {
|
||||
const cmd = createStartupScriptCommand("npx:foo", {});
|
||||
expect(cmd).toBe("npx foo");
|
||||
});
|
||||
it("should parse npx command (with -)", () => {
|
||||
const cmd = createStartupScriptCommand("npx:start-foo", {});
|
||||
expect(cmd).toBe("npx start-foo");
|
||||
});
|
||||
it("should parse npx command (with :)", () => {
|
||||
const cmd = createStartupScriptCommand("npx:start:foo", {});
|
||||
expect(cmd).toBe("npx start:foo");
|
||||
});
|
||||
it("should parse npx command (with #)", () => {
|
||||
const cmd = createStartupScriptCommand("npx:start#foo", {});
|
||||
expect(cmd).toBe("npx start#foo");
|
||||
});
|
||||
});
|
||||
describe("npm, npm and npx with optional args", () => {
|
||||
it("should parse npm options", () => {
|
||||
const cmd = createStartupScriptCommand("npm:foo --foo1 --foo2", {});
|
||||
expect(cmd).toBe("npm run foo --foo1 --foo2 --if-present");
|
||||
});
|
||||
it("should parse yarn options", () => {
|
||||
const cmd = createStartupScriptCommand("yarn:foo --foo1 --foo2", {});
|
||||
expect(cmd).toBe("yarn run foo --foo1 --foo2 --if-present");
|
||||
});
|
||||
it("should parse npx options", () => {
|
||||
const cmd = createStartupScriptCommand("npx:foo --foo1 --foo2", {});
|
||||
expect(cmd).toBe("npx foo --foo1 --foo2");
|
||||
});
|
||||
});
|
||||
describe("an external script", () => {
|
||||
it("should parse relative script file ./script.sh", () => {
|
||||
mockFs({
|
||||
"script.sh": "",
|
||||
});
|
||||
const cmd = createStartupScriptCommand("script.sh", {});
|
||||
expect(cmd).toBe(`${process.cwd()}${path.sep}script.sh`);
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("should parse relative script file ./script.sh from the root of --app-location", () => {
|
||||
mockFs({
|
||||
"/bar/script.sh": "",
|
||||
});
|
||||
const cmd = createStartupScriptCommand("script.sh", { appLocation: `${path.sep}bar` });
|
||||
expect(cmd).toInclude(path.join(path.sep, "bar", "script.sh"));
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("should parse absolute script file /foo/script.sh", () => {
|
||||
mockFs({
|
||||
"/foo": {
|
||||
"script.sh": "",
|
||||
},
|
||||
});
|
||||
const cmd = createStartupScriptCommand("/foo/script.sh", {});
|
||||
expect(cmd).toBe("/foo/script.sh");
|
||||
mockFs.restore();
|
||||
});
|
||||
});
|
||||
describe("non-valid use cases", () => {
|
||||
it("should handle non-valid npm patterns", () => {
|
||||
const cmd = createStartupScriptCommand("npm", {});
|
||||
expect(cmd).toBe(null);
|
||||
});
|
||||
it("should handle non-valid yarn patterns", () => {
|
||||
const cmd = createStartupScriptCommand("yarn", {});
|
||||
expect(cmd).toBe(null);
|
||||
});
|
||||
it("should handle non-valid npx patterns", () => {
|
||||
const cmd = createStartupScriptCommand("npx", {});
|
||||
expect(cmd).toBe(null);
|
||||
});
|
||||
it("should handle non-existant scripts (relative)", () => {
|
||||
const cmd = createStartupScriptCommand("script.sh", {});
|
||||
expect(cmd).toBe(null);
|
||||
});
|
||||
it("should handle non-existant scripts (asbolute)", () => {
|
||||
const cmd = createStartupScriptCommand("/foo/bar/script.sh", {});
|
||||
expect(cmd).toBe(null);
|
||||
});
|
||||
it("should handle non-existant scripts (asbolute)", () => {
|
||||
const cmd = createStartupScriptCommand(`"npm:µ˜¬…˚πº–ª¶§∞¢£¢™§_)(*!#˜%@)`, {});
|
||||
expect(cmd).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -506,3 +506,27 @@ export const registerProcessExit = (fn: Function) => {
|
|||
process.on("SIGTERM", wrapper);
|
||||
process.on("exit", wrapper);
|
||||
};
|
||||
|
||||
export const createStartupScriptCommand = (startupScript: string, options: SWACLIConfig) => {
|
||||
if (startupScript.includes(":")) {
|
||||
const [npmOrYarnBin, ...npmOrYarnScript] = startupScript.split(":");
|
||||
if (["npm", "yarn"].includes(npmOrYarnBin)) {
|
||||
return `${npmOrYarnBin} run ${npmOrYarnScript.join(":")} --if-present`;
|
||||
} else if (["npx"].includes(npmOrYarnBin)) {
|
||||
return `${npmOrYarnBin} ${npmOrYarnScript.join(":")}`;
|
||||
}
|
||||
} else {
|
||||
if (!path.isAbsolute(startupScript)) {
|
||||
const { appLocation } = options;
|
||||
const cwd = appLocation || process.cwd();
|
||||
startupScript = path.resolve(cwd, startupScript);
|
||||
}
|
||||
|
||||
if (fs.existsSync(startupScript)) {
|
||||
return startupScript;
|
||||
} else {
|
||||
logger.error(`Script file "${startupScript}" was not found.`, true);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ declare global {
|
|||
SWA_CLI_APP_SSL: boolean;
|
||||
SWA_CLI_APP_SSL_KEY: string;
|
||||
SWA_CLI_APP_SSL_CERT: string;
|
||||
SWA_CLI_STARTUP_COMMAND: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +70,7 @@ declare type SWACLIConfig = GithubActionWorkflow & {
|
|||
api?: string;
|
||||
build?: boolean;
|
||||
verbose?: string;
|
||||
run?: string;
|
||||
};
|
||||
|
||||
declare type ResponseOptions = {
|
||||
|
|
Загрузка…
Ссылка в новой задаче