Skip to content

Commit

Permalink
feat: start the emulator even if a GA isn't found (#35)
Browse files Browse the repository at this point in the history
Closes #15
  • Loading branch information
manekinekko committed Dec 4, 2020
1 parent 8e1330f commit e13afed
Show file tree
Hide file tree
Showing 13 changed files with 958 additions and 704 deletions.
10 changes: 9 additions & 1 deletion environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ declare global {
SWA_EMU_API_URI: string;
SWA_EMU_API_PREFIX: string;
SWA_EMU_APP_URI: string;
SWA_EMU_APP_LOCATION: string;
SWA_EMU_APP_ARTIFACT_LOCATION: string;
SWA_EMU_HOST: string;
SWA_EMU_PORT: string;
}
}
}

export {};

declare interface RuntimeHostConfig {
appPort: number;
proxyHost: string;
proxyPort: number;
appLocation: string | undefined;
appArtifactLocation: string | undefined;
};
14 changes: 11 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
"roots": [
"<rootDir>/src"
],
"testMatch": [
"**/__tests__/**/*.+(ts)",
"**/?(*.)+(spec|test).+(ts)"
],
"transform": {
"^.+\\.(ts)$": "ts-jest"
},
}
1,181 changes: 615 additions & 566 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Azure Static Web Apps Emulator for Auth, API and static content",
"scripts": {
"release": "release-it --preRelease=alpha",
"test": "jest",
"test": "jest --detectOpenHandles --silent --verbose",
"build": "tsc",
"prebuild": "rm -fr dist",
"watch": "tsc --watch"
Expand Down Expand Up @@ -32,17 +32,17 @@
"@types/blessed": "^0.1.17",
"@types/cookie": "^0.4.0",
"@types/http-proxy": "^1.17.4",
"@types/jest": "^26.0.15",
"@types/jest": "^26.0.16",
"@types/jsonwebtoken": "^8.5.0",
"@types/mock-fs": "^4.13.0",
"@types/node-fetch": "^2.5.7",
"@types/shelljs": "^0.8.8",
"jest": "^26.4.2",
"jest": "^26.6.3",
"mock-fs": "^4.13.0",
"release-it": "^13.6.4",
"supertest": "^4.0.2",
"ts-jest": "^26.4.3",
"typescript": "^4.0.3"
"ts-jest": "^26.4.4",
"typescript": "^4.1.2"
},
"homepage": "https://github.com/manekinekko/swa-emulator#readme",
"private": false,
Expand Down
74 changes: 38 additions & 36 deletions src/builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "fs";
import path from "path";
import shell from "shelljs";
import { readConfigFile } from "./utils";
import { readConfigFile, GithubActionSWAConfig } from "./utils";
import { detectRuntime, RuntimeType } from "./runtimes";

const exec = (command: string, options = {}) => shell.exec(command, { async: false, ...options });
Expand Down Expand Up @@ -39,49 +39,51 @@ const dotnetBuilder = (location: string, name: string, colour: string) => {
});
};

const builder = () => {
const { app_location, api_location, app_build_command, api_build_command } = readConfigFile();
const runtimeType = detectRuntime(app_location);
const builder = ({config}: {config: Partial<GithubActionSWAConfig>}) => {
const configFile = readConfigFile();
if (configFile) {
let { appLocation, apiLocation, appBuildCommand, apiBuildCommand } = config as GithubActionSWAConfig;
const runtimeType = detectRuntime(appLocation);

try {
switch (runtimeType) {
case RuntimeType.dotnet:
{
// build app
dotnetBuilder(app_location, "app_build", "bgGreen.bold");
try {
switch (runtimeType) {
case RuntimeType.dotnet:
{
// build app
dotnetBuilder(appLocation, "app_build", "bgGreen.bold");

// NOTE: API is optional. Build it only if it exists
// This may result in a double-compile of some libraries if they are shared between the
// Blazor app and the API, but it's an acceptable outcome
let apiLocation = path.resolve(process.cwd(), api_location);
if (fs.existsSync(apiLocation) === true && fs.existsSync(path.join(apiLocation, "host.json"))) {
dotnetBuilder(apiLocation, "api_build", "bgYellow.bold");
// NOTE: API is optional. Build it only if it exists
// This may result in a double-compile of some libraries if they are shared between the
// Blazor app and the API, but it's an acceptable outcome
apiLocation = path.resolve(process.cwd(), apiLocation);
if (fs.existsSync(apiLocation) === true && fs.existsSync(path.join(apiLocation, "host.json"))) {
dotnetBuilder(apiLocation, "api_build", "bgYellow.bold");
}
}
}
break;
break;

case RuntimeType.node:
default:
{
// figure out if appLocation exists
let appLocation = app_location;
if (fs.existsSync(appLocation) === false) {
appLocation = process.cwd();
}
case RuntimeType.node:
default:
{
// figure out if appLocation exists
if (fs.existsSync(appLocation) === false) {
appLocation = process.cwd();
}

// build app
nodeBuilder(appLocation, app_build_command, "app_build", "bgGreen.bold");
// build app
nodeBuilder(appLocation, appBuildCommand, "app_build", "bgGreen.bold");

// NOTE: API is optional. Build it only if it exists
let apiLocation = path.resolve(process.cwd(), api_location);
if (fs.existsSync(apiLocation) === true && fs.existsSync(path.join(apiLocation, "host.json"))) {
nodeBuilder(apiLocation, api_build_command, "api_build", "bgYellow.bold");
// NOTE: API is optional. Build it only if it exists
apiLocation = path.resolve(process.cwd(), apiLocation);
if (fs.existsSync(apiLocation) === true && fs.existsSync(path.join(apiLocation, "host.json"))) {
nodeBuilder(apiLocation, apiBuildCommand, "api_build", "bgYellow.bold");
}
}
}
break;
break;
}
} catch (stderr) {
shell.echo(stderr);
}
} catch (stderr) {
console.error(stderr);
}
};
export default builder;
76 changes: 64 additions & 12 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/usr/bin/env node

import shell from "shelljs";
import path from "path";
import { spawn } from "child_process";
import program from "commander";
import path from "path";
import shell from "shelljs";
import builder from "./builder";
import { readConfigFile } from "./utils";
import { spawn } from "child_process";
import { createRuntimeHost } from "./runtimeHost";
import { Dashboard } from "./dashboard";
import { createRuntimeHost } from "./runtimeHost";
import { GithubActionSWAConfig, readConfigFile } from "./utils";

const EMU_PORT = "80";
const AUTH_PORT = 4242;
Expand All @@ -18,6 +18,13 @@ program
.name("swa")
.usage("<command>")
.version(require("../package.json").version)

// SWA config
.option("--app-location <appLocation>", "set app folder (location for the application code)", undefined)
.option("--app-artifact-location <appArtifactLocation>", "set app artifact folder (location where files are built for production)", undefined)
.option("--api-location <apiLocation>", "set the API folder", undefined)

// Emulator config
.option("--auth-uri <authUri>", "set Auth uri", `http://localhost:${AUTH_PORT}`)
.option("--api-uri <apiUri>", "set API uri", `http://localhost:${API_PORT}`)
.option("--api-prefix <apiPrefix>", "set API prefix", "api")
Expand All @@ -44,7 +51,42 @@ const appUriPort = appUriSegments[2] || APP_PORT;

// provide binaries
const concurrentlyBin = path.resolve(__dirname, "..", "./node_modules/.bin/concurrently");
const { app_artifact_location, api_location } = readConfigFile();

// get the app and api artifact locations
let [appLocation, appArtifactLocation, apiLocation] = [
program.appLocation as string,
program.appArtifactLocation as string,
program.apiLocation as string,
];

// retrieve the project's build configuration
// provide any specific config that the user might provide
const configFile = readConfigFile({
overrideConfig: {
appLocation,
appArtifactLocation,
},
});

// double check the required options are defined before moving on.
// apply defaults otherwise
if (configFile) {
if (configFile.appLocation === undefined) {
console.warn(`WARNING: The app location (--app-location) was not provided. Using "./" as a default value.`);
configFile.appLocation = "./";
}

if (configFile.appArtifactLocation === undefined) {
console.warn(`WARNING: The app artifact location (--app-artifact-location) was not provided. Using "./" as a default value.`);
configFile.appArtifactLocation = "./";
}

// set the default value for api location
if (configFile.apiLocation === undefined) {
console.warn(`WARNING: The api location (--api-location) was not provided. Using "./api" as a default value.`);
configFile.apiLocation = `./api`;
}
}

const envVarsObj = {
// set env vars for current command
Expand All @@ -60,14 +102,22 @@ const envVarsObj = {
SWA_EMU_API_URI: program.useApi || program.apiUri,
SWA_EMU_API_PREFIX: program.apiPrefix,
SWA_EMU_APP_URI: program.useApp || program.appUri,
SWA_EMU_APP_LOCATION: app_artifact_location,
SWA_EMU_APP_LOCATION: configFile?.appLocation as string,
SWA_EMU_APP_ARTIFACT_LOCATION: configFile?.appArtifactLocation as string,
SWA_EMU_API_LOCATION: configFile?.apiLocation as string,
SWA_EMU_HOST: program.host,
SWA_EMU_PORT: program.port,
};

const { command: hostCommand, args: hostArgs } = createRuntimeHost(appUriPort, program.host, program.port);
const { command: hostCommand, args: hostArgs } = createRuntimeHost({
appPort: appUriPort,
proxyHost: program.host,
proxyPort: program.port,
appLocation: configFile?.appLocation,
appArtifactLocation: configFile?.appArtifactLocation,
});

let serveApiContent = `[ -d '${api_location}' ] && (cd ${api_location}; func start --cors *) || echo 'No API found. Skipping.'`;
let serveApiContent = `[ -d '${apiLocation}' ] && (cd ${apiLocation}; func start --cors *) || echo 'No API found. Skipping.'`;
if (program.useApi) {
serveApiContent = `echo 'using API dev server at ${program.useApi}'`;
}
Expand Down Expand Up @@ -100,12 +150,14 @@ const startCommand = [
];

if (process.env.DEBUG) {
console.log(startCommand);
shell.echo(startCommand.join("\n"));
}

if (program.build) {
// run the app/api builds
builder();
builder({
config: configFile as GithubActionSWAConfig,
});
}

if (program.ui) {
Expand All @@ -124,7 +176,7 @@ if (program.ui) {
dashboard.stream("hosting", hosting);

// start functions
const functions = spawnx(`[ -d '${api_location}' ] && (cd ${api_location}; func start --cors *) || echo 'No API found. Skipping.'`, []);
const functions = spawnx(`[ -d '${apiLocation}' ] && (cd ${apiLocation}; func start --cors *) || echo 'No API found. Skipping.'`, []);
dashboard.stream("functions", functions);

// start auth
Expand Down
4 changes: 2 additions & 2 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const readRoutes = (folder: string): UserDefinedRoute[] => {
return require(path.join(folder, routesFile)).routes || [];
};

const routes = readRoutes(process.env.SWA_EMU_APP_LOCATION || "");
const routes = readRoutes(process.env.SWA_EMU_APP_ARTIFACT_LOCATION || "");

const routeTest = (userDefinedRoute: string, currentRoute: string) => {
if (userDefinedRoute === currentRoute) {
Expand Down Expand Up @@ -149,7 +149,7 @@ const server = http.createServer(function (req, res) {
// detected SPA mode
else if (req.url.startsWith("/?")) {
console.log("proxy>", req.method, req.headers.host + req.url);
const fileIndex = path.join(process.env.SWA_EMU_APP_LOCATION, "index.html");
const fileIndex = path.join(process.env.SWA_EMU_APP_ARTIFACT_LOCATION, "index.html");
serveStatic(fileIndex, res);
}

Expand Down
76 changes: 76 additions & 0 deletions src/runtimeHost.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createRuntimeHost } from "./runtimeHost";
import * as detectRuntime from "./runtimes";

let spyDetectRuntime: jest.SpyInstance;
const mockConfig = {
appPort: 8080,
appArtifactLocation: "./",
appLocation: "./",
proxyHost: "0.0.0.0",
proxyPort: 4242,
};

describe("runtimeHost", () => {
beforeEach(() => {
process.env.DEBUG = "";
spyDetectRuntime = jest.spyOn(detectRuntime, "detectRuntime");
spyDetectRuntime.mockReturnValue(detectRuntime.RuntimeType.unknown);
});

describe("createRuntimeHost()", () => {
it("appArtifactLocation should be propagated in resulting command", () => {
const rh = createRuntimeHost({
...mockConfig,
appArtifactLocation: "./foobar",
});

expect(spyDetectRuntime).toHaveBeenCalledWith("./");
expect(rh.command).toContain("@manekinekko/swa-emulator/node_modules/.bin/http-server");
expect(rh.args).toEqual(["./foobar", "-p", "8080", "-c-1", "--proxy", "http://0.0.0.0:4242/?"]);
});

it("appArtifactLocation should default to ./ if undefined", () => {
const rh = createRuntimeHost({
...mockConfig,
appArtifactLocation: undefined,
});

expect(spyDetectRuntime).toHaveBeenCalledWith("./");
expect(rh.command).toContain("@manekinekko/swa-emulator/node_modules/.bin/http-server");
expect(rh.args).toEqual(["./", "-p", "8080", "-c-1", "--proxy", "http://0.0.0.0:4242/?"]);
});

it("proxyHost should be propagated in resulting command", () => {
const rh = createRuntimeHost({
...mockConfig,
proxyHost: "127.0.0.1",
});

expect(spyDetectRuntime).toHaveBeenCalledWith("./");
expect(rh.command).toContain("@manekinekko/swa-emulator/node_modules/.bin/http-server");
expect(rh.args).toEqual(["./", "-p", "8080", "-c-1", "--proxy", "http://127.0.0.1:4242/?"]);
});

it("proxyPort should be propagated in resulting command", () => {
const rh = createRuntimeHost({
...mockConfig,
proxyPort: 3000,
});

expect(spyDetectRuntime).toHaveBeenCalledWith("./");
expect(rh.command).toContain("@manekinekko/swa-emulator/node_modules/.bin/http-server");
expect(rh.args).toEqual(["./", "-p", "8080", "-c-1", "--proxy", "http://0.0.0.0:3000/?"]);
});

it("appLocation should be propagated to the runtime detector", () => {
const rh = createRuntimeHost({
...mockConfig,
appLocation: "./foobar",
});

expect(spyDetectRuntime).toHaveBeenCalledWith("./foobar");
expect(rh.command).toContain("@manekinekko/swa-emulator/node_modules/.bin/http-server");
expect(rh.args).toEqual(["./", "-p", "8080", "-c-1", "--proxy", "http://0.0.0.0:4242/?"]);
});
});
});

0 comments on commit e13afed

Please sign in to comment.