зеркало из
1
0
Форкнуть 0

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:
Wassim Chegham 2021-03-26 15:36:43 +01:00 коммит произвёл GitHub
Родитель d59a7decbd
Коммит 3c88860e8a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 217 добавлений и 20 удалений

Просмотреть файл

@ -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;
};

2
src/swa.d.ts поставляемый
Просмотреть файл

@ -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 = {