Skip to content

Commit

Permalink
feat: https support for dev reverse proxy (#1572)
Browse files Browse the repository at this point in the history
jchip authored Mar 23, 2020
1 parent 6c34654 commit 6aaed71
Showing 28 changed files with 888 additions and 443 deletions.
142 changes: 142 additions & 0 deletions docs/guides/local-https-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Localhost HTTPS Setup

These instructions are for MacOS, verified on macOS Mojave version 10.14.6

## SSL Key and Certificate

1. Generate SSL key and cert

Copy and paste these commands in the terminal to run them.

> Make sure to change the hostname `dev.mydomain.com` in both places to your desired value.
```bash
openssl req -new -x509 -nodes -sha256 -days 3650 \
-newkey rsa:2048 -out dev-proxy.crt -keyout dev-proxy.key \
-extensions SAN -reqexts SAN -subj /CN=dev.mydomain.com \
-config <(cat /etc/ssl/openssl.cnf \
<(printf '[ext]\nbasicConstraints=critical,CA:TRUE,pathlen:0\n') \
<(printf '[SAN]\nsubjectAltName=DNS:dev.mydomain.com,IP:127.0.0.1\n'))
```

2. Add cert to your system keychain

```bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain dev-proxy.crt
```

3. Put the files `dev-proxy.key` and `dev-proxy.crt` in your app's dir.

> Alternatively, you can put them in one of the directories listed below:
- `src`
- `test`
- `config`

## Development in HTTPS

After everything's setup, you can start development in HTTPS with the following steps:

1. Using your favorite editor, add this line to your `/etc/hosts` file.

> Change the hostname `dev.mydomain.com` accordingly if you used a different one.
```
127.0.0.1 dev.mydomain.com
```

2. Now to run app dev in HTTPS, set the env `ELECTRODE_DEV_HTTPS` to `8443` and `HOST` to the domain name you created your cert for.

- Example: `HOST=dev.mydomain.com ELECTRODE_DEV_HTTPS=8443 npm run dev`

- And point your browser to `https://dev.mydomain.com:8443`

- If you have access to listen on the standard HTTPS port `443`, then you can set it to `443` or `true`, and use the URL `https://dev.mydomain.com` directly.

- Another way to trigger HTTPS is with the env `PORT`. If that is set to `443` exactly, then the dev proxy will enter HTTPS mode even if env `ELECTRODE_DEV_HTTPS` is not set.

## Elevated Privilege

Generally, normal users can't run program to listen on network port below 1024.

> but that seems to have changed for MacOS Mojave https://news.ycombinator.com/item?id=18302380
So if you want to set the dev proxy to listen on the standard HTTP port 80 or HTTPS port 443, you might need to give it elevated access.

The recommended approach to achieve this is to run the dev proxy in a separate terminal with elevated access:

```bash
sudo HOST=dev.mydomain.com PORT=443 npx clap dev-proxy
```

And then start normal development in another terminal:

```bash
HOST=dev.mydomain.com npm run dev
```

### Automatic Elevating (optional)

Optional: for best result, please use the manual approach recommended above.

If your machine requires elevated access for the proxy to listen at a port, then a dialog will pop up to ask you for your password. This is achieved with the module https://www.npmjs.com/package/sudo-prompt

This requirement is automatically detected, but if you want to explicitly trigger the elevated access, you can set the env `ELECTRODE_DEV_ELEVATED` to `true`.

> However, due to restrictions with acquiring elevated access, this automatic acquisition has quirks. For example, the logs from the dev proxy can't be shown in your console.
## Custom Proxy Rules

The dev proxy is using a slightly modified version of [redbird] with some fixes and enhancements that are pending PR merge.

You can provide your own proxy rules with a file `dev-proxy-rules.js` in one of these directories:

- `src`
- `test`
- `config`

The file should export a function `setupRules`, like the example below:

```js
export function setupRules(proxy, options) {
const { host, port, protocol } = options;
proxy.register(
`${protocol}://${host}:${port}/myapi/foo-api`,
`https://api.myserver.com/myapi/foo-api`
);
}
```

Where:

- `proxy` - the redbird proxy instance.
- `options` - all configuration for the proxy:

- `host` - hostname proxy is using.
- `port` - primary port proxy is listening on.
- `appPort` - app server's port the proxy forward to.
- `httpPort` - HTTP port proxy is listening on.
- `httpsPort` - Port for HTTPS if it is enabled.
- `https` - `true`/`false` to indicate if proxy is running in HTTPS mode.
- `webpackDev` - `true`/`false` to indicate if running with webpack dev.
- `webpackDevPort` - webpack dev server's port the proxy is forwarding to.
- `webpackDevPort` - webpack dev server's host the proxy is forwarding to.
- `protocol` - primary protocol: `"http"` or `"https"`.
- `elevated` - `true`/`false` to indicate if proxy should acquire elevate access.

- The primary protocol is `https` if HTTPS is enabled, else it's `http`.

- `appPort` is the port your app server is expected to listen on. It's determined as follows:

1. env `APP_SERVER_PORT` or `APP_PORT_FOR_PROXY` if it's a valid number.
2. fallback to `3100`.

- Even if HTTPS is enabled, the proxy always listens on HTTP also. In that case, `httpPort` is determined as follows:

1. env `PORT` if it's defined
2. if `appPort` is not `3000`, then fallback to `3000`.
3. finally fallback to `3300`.

The primary API to register your proxy rule is [`proxy.register`](https://www.npmjs.com/package/redbird#redbirdregistersrc-target-opts).

[redbird]: https://www.npmjs.com/package/redbird
19 changes: 7 additions & 12 deletions packages/subapp-server/lib/setup-hapi-routes.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@ const _ = require("lodash");
const Fs = require("fs");
const Path = require("path");
const assert = require("assert");
const Url = require("url");
const util = require("util");
const optionalRequire = require("optional-require")(require);
const scanDir = require("filter-scan-dir");
@@ -139,11 +138,9 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) {
return h.continue;
});

topOpts.devBundleBase = Url.format({
protocol: topOpts.devServer.https ? "https" : "http",
hostname: topOpts.devServer.host,
port: topOpts.devServer.port,
pathname: "/js/"
topOpts.devBundleBase = subAppUtil.formUrl({
..._.pick(topOpts.devServer, ["protocol", "host", "port"]),
path: "/js/"
});

// register routes
@@ -192,7 +189,7 @@ async function setupRoutesFromFile(srcDir, server, pluginOpts) {
.type("text/html; charset=UTF-8")
.code(200);
} else if (HttpStatus.redirect[status]) {
return h.redirect(data.path);
return h.redirect(data.path).code(status);
} else if (HttpStatus.displayHtml[status]) {
return h.response(data.html !== undefined ? data.html : data).code(status);
} else if (status >= 200 && status < 300) {
@@ -256,11 +253,9 @@ async function setupRoutesFromDir(server, pluginOpts, fromDir) {
});
}

topOpts.devBundleBase = Url.format({
protocol: topOpts.devServer.https ? "https" : "http",
hostname: topOpts.devServer.host,
port: topOpts.devServer.port,
pathname: "/js/"
topOpts.devBundleBase = subAppUtil.formUrl({
..._.pick(topOpts.devServer, ["protocol", "host", "port"]),
path: "/js/"
});

registerRoutes({ routes, topOpts, server });
30 changes: 13 additions & 17 deletions packages/subapp-server/lib/utils.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,13 @@

const Fs = require("fs");
const Path = require("path");
const optionalRequire = require("optional-require")(require);
const {
settings = {},
devServer = {},
fullDevServer = {},
httpDevServer = {}
} = optionalRequire("@xarc/app-dev/config/dev-proxy", { default: {} });

/**
* Tries to import bundle chunk selector function if the corresponding option is set in the
@@ -55,33 +62,22 @@ const updateFullTemplate = (baseDir, options) => {
}
};

function findEnv(keys, defVal) {
const k = [].concat(keys).find(x => x && process.env.hasOwnProperty(x));
return k ? process.env[k] : defVal;
}

function getDefaultRouteOptions() {
const isDevProxy = process.env.hasOwnProperty("APP_SERVER_PORT");
const webpackDev = process.env.WEBPACK_DEV === "true";
const { webpackDev, useDevProxy } = settings;
// temporary location to write build artifacts in dev mode
const buildArtifacts = ".etmp";
return {
pageTitle: "Untitled Electrode Web Application",
//
webpackDev,
isDevProxy,
//
devServer: {
host: findEnv([isDevProxy && "HOST", "WEBPACK_DEV_HOST", "WEBPACK_HOST"], "127.0.0.1"),
port: findEnv([isDevProxy && "PORT", "WEBPACK_DEV_PORT"], isDevProxy ? "3000" : "2992"),
https: Boolean(process.env.WEBPACK_DEV_HTTPS)
},
//
useDevProxy,
devServer,
fullDevServer,
httpDevServer,
stats: webpackDev ? `${buildArtifacts}/stats.json` : "dist/server/stats.json",
iconStats: "dist/server/iconstats.json",
criticalCSS: "dist/js/critical.css",
buildArtifacts,
prodBundleBase: "/js/",
prodBundleBase: "/js",
devBundleBase: "/js",
cspNonceValue: undefined,
templateFile: Path.join(__dirname, "..", "resources", "index-page")
22 changes: 21 additions & 1 deletion packages/subapp-util/lib/index.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

/* eslint-disable no-console, no-process-exit, max-params */

const Url = require("url");
const Path = require("path");
const assert = require("assert");
const optionalRequire = require("optional-require")(require);
@@ -289,6 +290,24 @@ function refreshAllSubApps() {
}
}

const formUrl = ({ protocol = "http", host = "", port = "", path = "" }) => {
let proto = protocol.toString().toLowerCase();
let host2 = host;

if (port) {
const sp = port.toString();
if (sp === "80") {
proto = "http";
} else if (sp === "443") {
proto = "https";
} else if (host) {
host2 = `${host}:${port}`;
}
}

return Url.format({ protocol: proto, host: host2, pathname: path });
};

module.exports = {
es6Require,
scanSubAppsFromDir,
@@ -300,5 +319,6 @@ module.exports = {
loadSubAppByName,
loadSubAppServerByName,
refreshSubAppByName,
refreshAllSubApps
refreshAllSubApps,
formUrl
};
55 changes: 40 additions & 15 deletions packages/subapp-web/lib/load.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";

/* eslint-disable max-statements, no-console, complexity */
/* eslint-disable max-statements, no-console, complexity, no-magic-numbers */

/*
* - Figure out all the dependencies and bundles a subapp needs and make sure
@@ -19,20 +19,34 @@ const retrieveUrl = require("request");
const util = require("./util");
const xaa = require("xaa");
const jsesc = require("jsesc");
const { loadSubAppByName, loadSubAppServerByName } = require("subapp-util");
const { loadSubAppByName, loadSubAppServerByName, formUrl } = require("subapp-util");

// global name to store client subapp runtime, ie: window.xarcV1
// V1: version 1.
const xarc = "window.xarcV1";

// Size threshold of initial state string to embed it as a application/json script tag
// It's more efficent to JSON.parse large JSON data instead of embedding them as JS.
// It's more efficient to JSON.parse large JSON data instead of embedding them as JS.
// https://quipblog.com/efficiently-loading-inlined-json-data-911960b0ac0a
// > The data sizes are as follows: large is 1.7MB of JSON, medium is 130K,
// > small is 10K and tiny is 781 bytes.
const INITIAL_STATE_SIZE_FOR_JSON = 1024;
let INITIAL_STATE_TAG_ID = 0;

const makeDevDebugMessage = (msg, reportLink = true) => {
const reportMsg = reportLink
? `\nError: Please capture this info and submit a bug report at https://github.com/electrode-io/electrode`
: "";
return `Error: at ${util.removeCwd(__filename)}
${msg}${reportMsg}`;
};

const makeDevDebugHtml = msg => {
return `<h1 style="background-color: red">DEV ERROR</h1>
<p><pre style="color: red">${msg}</pre></p>
<!-- ${msg} -->`;
};

module.exports = function setup(setupContext, { props: setupProps }) {
// TODO: create JSON schema to validate props

@@ -61,10 +75,18 @@ module.exports = function setup(setupContext, { props: setupProps }) {
// to inline in the index page.
//
const retrieveDevServerBundle = async () => {
return new Promise((resolve, reject) => {
retrieveUrl(`${bundleBase}${bundleAsset.name}`, (err, resp, body) => {
if (err) {
reject(err);
return new Promise(resolve => {
const routeOptions = setupContext.routeOptions;
const path = `${bundleBase}${bundleAsset.name}`;
const bundleUrl = formUrl({ ...routeOptions.httpDevServer, path });
retrieveUrl(bundleUrl, (err, resp, body) => {
if (err || resp.statusCode !== 200) {
const msg = makeDevDebugMessage(
`Error: fail to retrieve subapp bundle from '${bundleUrl}' for inlining in index HTML
${err || body}`
);
console.error(msg); // eslint-disable-line
resolve(makeDevDebugHtml(msg));
} else {
resolve(`<script>/*${name}*/${body}</script>`);
}
@@ -81,7 +103,7 @@ module.exports = function setup(setupContext, { props: setupProps }) {
let inlineSubAppJs;

const prepareSubAppJsBundle = () => {
const webpackDev = process.env.WEBPACK_DEV === "true";
const { webpackDev } = setupContext.routeOptions;

if (setupProps.inlineScript === "always" || (setupProps.inlineScript === true && !webpackDev)) {
if (!webpackDev) {
@@ -93,7 +115,9 @@ module.exports = function setup(setupContext, { props: setupProps }) {
} else if (ext === ".css") {
inlineSubAppJs = `<style id="${name}">${src}</style>`;
} else {
inlineSubAppJs = `<!-- UNKNOWN bundle extension ${name} -->`;
const msg = makeDevDebugMessage(`Error: UNKNOWN bundle extension ${name}`);
console.error(msg); // eslint-disable-line
inlineSubAppJs = makeDevDebugHtml(msg);
}
} else {
inlineSubAppJs = true;
@@ -251,12 +275,13 @@ ${dynInitialState}<script>${xarc}.startSubAppOnLoad({
const handleError = err => {
if (process.env.NODE_ENV !== "production") {
const stack = util.removeCwd(err.stack);
console.error(`SSR subapp ${name} failed <error>${stack}</error>`); // eslint-disable-line
outputSpot.add(`<!-- SSR subapp ${name} failed
${stack}
-->`);
const msg = makeDevDebugMessage(
`Error: SSR subapp ${name} failed
${stack}`,
false // SSR failure is likely an issue in user code, don't show link to report bug
);
console.error(msg); // eslint-disable-line
outputSpot.add(makeDevDebugHtml(msg));
} else if (request && request.log) {
request.log(["error"], { msg: `SSR subapp ${name} failed`, err });
}
30 changes: 0 additions & 30 deletions packages/xarc-app-dev/.eslintrc

This file was deleted.

124 changes: 21 additions & 103 deletions packages/xarc-app-dev/config/archetype.js
Original file line number Diff line number Diff line change
@@ -1,124 +1,29 @@
"use strict";

const Path = require("path");
const optionalRequire = require("optional-require")(require);
const userConfig = Object.assign({}, optionalRequire(Path.resolve("archetype/config")));
const { merge } = require("lodash");

const devPkg = require("../package.json");
const devDir = Path.join(__dirname, "..");
const devRequire = require(`../require`);
const configDir = `${devDir}/config`;
const xenvConfig = devRequire("xenv-config");
const xenvConfig = require("xenv-config");
const _ = require("lodash");

const defaultOptimizeCssOptions = {
cssProcessorOptions: {
zindex: false
}
};
const options = (userConfig && userConfig.options) || {};

const webpackConfigSpec = {
devHostname: { env: ["WEBPACK_HOST", "WEBPACK_DEV_HOST"], default: "localhost" },
devPort: { env: "WEBPACK_DEV_PORT", default: 2992 },
// Using a built-in reverse proxy, the webpack dev assets are served from the
// same host and port as the app. In that case, the URLs to assets are relative
// without protocol, host, port.
// However, user can simulate CDN server with the proxy and have assets URLs
// specifying different host/port from the app. To do that, the following
// should be defined.
cdnProtocol: { env: ["WEBPACK_DEV_CDN_PROTOCOL"], type: "string", default: null },
cdnHostname: { env: ["WEBPACK_DEV_CDN_HOST"], type: "string", default: null },
cdnPort: { env: ["WEBPACK_DEV_CDN_PORT"], default: 0 },
//
// in dev mode, all webpack output are saved to memory only, but some files like
// stats.json are needed by different uses and the stats partial saves a copy to
// disk. It will use this as the path to save the file.
devArtifactsPath: { env: "WEBPACK_DEV_ARTIFACTS_PATH", default: ".etmp" },
cssModuleSupport: { env: "CSS_MODULE_SUPPORT", type: "boolean", default: undefined },
enableBabelPolyfill: { env: "ENABLE_BABEL_POLYFILL", default: false },
enableNodeSourcePlugin: { env: "ENABLE_NODESOURCE_PLUGIN", default: false },
enableHotModuleReload: { env: "WEBPACK_HOT_MODULE_RELOAD", default: true },
enableWarningsOverlay: { env: "WEBPACK_DEV_WARNINGS_OVERLAY", default: true },
woffFontInlineLimit: { env: "WOFF_FONT_INLINE_LIMIT", default: 1000 },
preserveSymlinks: {
env: ["WEBPACK_PRESERVE_SYMLINKS", "NODE_PRESERVE_SYMLINKS"],
default: false
},
enableShortenCSSNames: { env: "ENABLE_SHORTEN_CSS_NAMES", default: true },
minimizeSubappChunks: { env: "MINIMIZE_SUBAPP_CHUNKS", default: false },
optimizeCssOptions: {
env: "OPTIMIZE_CSS_OPTIONS",
type: "json",
default: defaultOptimizeCssOptions
},
loadDlls: {
env: "ELECTRODE_LOAD_DLLS",
type: "json",
default: {}
},
minify: {
env: "WEBPACK_MINIFY",
default: true
}
};

const karmaConfigSpec = {
browser: { env: "KARMA_BROWSER", default: "chrome" }
};

const babelConfigSpec = {
enableTypeScript: { env: "ENABLE_BABEL_TYPESCRIPT", default: options.typescript || false },
enableDynamicImport: { env: "ENABLE_DYNAMIC_IMPORT", default: false },
enableFlow: { env: "ENABLE_BABEL_FLOW", default: true },
// require the @flow directive in source to enable FlowJS type stripping
flowRequireDirective: { env: "FLOW_REQUIRE_DIRECTIVE", default: false },
proposalDecorators: { env: "BABEL_PROPOSAL_DECORATORS", default: false },
legacyDecorators: { env: "BABEL_LEGACY_DECORATORS", default: true },
transformClassProps: { env: "BABEL_CLASS_PROPS", default: false },
looseClassProps: { env: "BABEL_CLASS_PROPS_LOOSE", default: true },
envTargets: {
env: "BABEL_ENV_TARGETS",
type: "json",
default: {
//`default` and `node` targets object is required
default: {
ie: "8"
},
node: process.versions.node.split(".")[0]
}
},
target: {
env: "ENV_TARGET",
type: "string",
default: "default"
},
// `extendLoader` is used to override `babel-loader` only when `hasMultiTargets=true`
extendLoader: {
type: "json",
default: {}
}
};
const userConfig = require("./user-config");

const topConfigSpec = {
devOpenBrowser: { env: "ELECTRODE_DEV_OPEN_BROWSER", default: false }
};
const typeScriptOption =
options.typescript === false
? {
babel: { enableTypeScript: options.typescript }
}
: {};
const webpack = require("./env-webpack");
const babel = require("./env-babel");
const karma = require("./env-karma");

const config = {
devDir,
devPkg,
devRequire,
webpack: xenvConfig(webpackConfigSpec, userConfig.webpack, { merge }),
karma: xenvConfig(karmaConfigSpec, userConfig.karma, { merge }),
webpack,
karma,
jest: Object.assign({}, userConfig.jest),
babel: xenvConfig(babelConfigSpec, userConfig.babel, { merge }),
babel,
config: Object.assign(
{},
{
@@ -133,6 +38,19 @@ const config = {
)
};

const topConfigSpec = {
devOpenBrowser: { env: "ELECTRODE_DEV_OPEN_BROWSER", default: false }
};

const { options } = userConfig;

const typeScriptOption =
options.typescript === false
? {
babel: { enableTypeScript: options.typescript }
}
: {};

module.exports = Object.assign(
_.merge(config, typeScriptOption),
xenvConfig(topConfigSpec, _.pick(userConfig, Object.keys(topConfigSpec)), { merge })
90 changes: 90 additions & 0 deletions packages/xarc-app-dev/config/dev-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use strict";

const Path = require("path");
const Fs = require("fs");

const envWebpack = require("./env-webpack");
const envApp = require("./env-app");
const envProxy = require("./env-proxy");

function searchSSLCerts() {
const searchDirs = ["", "config", "test", "src"];
for (const f of searchDirs) {
const key = Path.resolve(f, "dev-proxy.key");
const cert = Path.resolve(f, "dev-proxy.crt");
if (Fs.existsSync(key) && Fs.existsSync(cert)) {
return { key, cert };
}
}
return {};
}

const { host, portForProxy: appPort } = envApp;
const { webpackDev, devPort: webpackDevPort, devHostname: webpackDevHost } = envWebpack;

let protocol;
let port;
let httpPort = envApp.port;
let { httpsPort } = envProxy;
const { elevated } = envProxy;
const useDevProxy = appPort > 0;

if (httpsPort) {
port = httpsPort;
protocol = "https";
} else if (httpPort === 443) {
port = httpsPort = httpPort;
httpPort = appPort !== 3000 ? 3000 : 3300;
protocol = "https";
} else {
port = httpPort;
protocol = "http";
}

const settings = {
host,
port,
appPort,
httpPort,
httpsPort,
https: protocol === "https",
webpackDev,
webpackDevPort,
webpackDevHost,
protocol,
elevated,
useDevProxy
};

const adminPath = `/__proxy_admin`;
const hmrPath = `/__webpack_hmr`; // this is webpack-hot-middleware's default
const devPath = `/__electrode_dev`;

const controlPaths = {
admin: adminPath,
hmr: hmrPath,
dev: devPath,
status: `${adminPath}/status`,
exit: `${adminPath}/exit`,
restart: `${adminPath}/restart`,
appLog: `${devPath}/log`,
reporter: `${devPath}/reporter`
};

module.exports = {
settings,
devServer: useDevProxy
? // when using dev proxy, all routes and assets are unified at the same protocol/host/port
// so we can just use path to load assets and let browser figure out protocol/host/port
// from the location.
{ protocol: "", host: "", port: "" }
: // no dev proxy, so webpack dev server is running at a different port, so need to form
// full URL with protocol/host/port to get the assets.
{ protocol: "http", host: webpackDevHost, port: webpackDevPort, https: false },
fullDevServer: { protocol, host, port },
// If using dev proxy in HTTPS, then it's also listening on a HTTP port also:
httpDevServer: { protocol: "http", host, port: httpPort, https: false },
controlPaths,
searchSSLCerts,
certs: searchSSLCerts()
};
17 changes: 17 additions & 0 deletions packages/xarc-app-dev/config/env-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use strict";

const xenvConfig = require("xenv-config");
const { merge } = require("lodash");

const appConfigSpec = {
host: { env: ["HOST"], default: "localhost" },
port: { env: ["PORT"], default: 3000 },
portForProxy: {
env: ["APP_PORT_FOR_PROXY", "APP_SERVER_PORT"],
default: 0,
envMap: { false: 0, true: 3100 },
post: x => x || 0
}
};

module.exports = xenvConfig(appConfigSpec, {}, { merge });
42 changes: 42 additions & 0 deletions packages/xarc-app-dev/config/env-babel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use strict";

const xenvConfig = require("xenv-config");
const { merge } = require("lodash");

const userConfig = require("./user-config");
const { options } = userConfig;

const babelConfigSpec = {
enableTypeScript: { env: "ENABLE_BABEL_TYPESCRIPT", default: options.typescript || false },
enableDynamicImport: { env: "ENABLE_DYNAMIC_IMPORT", default: false },
enableFlow: { env: "ENABLE_BABEL_FLOW", default: true },
// require the @flow directive in source to enable FlowJS type stripping
flowRequireDirective: { env: "FLOW_REQUIRE_DIRECTIVE", default: false },
proposalDecorators: { env: "BABEL_PROPOSAL_DECORATORS", default: false },
legacyDecorators: { env: "BABEL_LEGACY_DECORATORS", default: true },
transformClassProps: { env: "BABEL_CLASS_PROPS", default: false },
looseClassProps: { env: "BABEL_CLASS_PROPS_LOOSE", default: true },
envTargets: {
env: "BABEL_ENV_TARGETS",
type: "json",
default: {
//`default` and `node` targets object is required
default: {
ie: "8"
},
node: process.versions.node.split(".")[0]
}
},
target: {
env: "ENV_TARGET",
type: "string",
default: "default"
},
// `extendLoader` is used to override `babel-loader` only when `hasMultiTargets=true`
extendLoader: {
type: "json",
default: {}
}
};

module.exports = xenvConfig(babelConfigSpec, userConfig.babel, { merge });
11 changes: 11 additions & 0 deletions packages/xarc-app-dev/config/env-karma.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";

const xenvConfig = require("xenv-config");
const { merge } = require("lodash");
const userConfig = require("./user-config");

const karmaConfigSpec = {
browser: { env: "KARMA_BROWSER", default: "chrome" }
};

module.exports = xenvConfig(karmaConfigSpec, userConfig.karma, { merge });
16 changes: 16 additions & 0 deletions packages/xarc-app-dev/config/env-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use strict";

const xenvConfig = require("xenv-config");
const { merge } = require("lodash");

const proxyConfigSpec = {
httpsPort: {
env: ["ELECTRODE_DEV_HTTPS", "XARC_DEV_HTTPS"],
default: 0,
envMap: { true: 443, false: 0 },
post: x => x || 0
},
elevated: { env: ["ELECTRODE_DEV_ELEVATED"], default: false }
};

module.exports = xenvConfig(proxyConfigSpec, {}, { merge });
50 changes: 50 additions & 0 deletions packages/xarc-app-dev/config/env-webpack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use strict";

const userConfig = require("./user-config");
const xenvConfig = require("xenv-config");
const { merge } = require("lodash");

const webpackConfigSpec = {
webpackDev: { env: "WEBPACK_DEV", default: false },
devHostname: { env: ["WEBPACK_HOST", "WEBPACK_DEV_HOST", "HOST"], default: "localhost" },
devPort: { env: "WEBPACK_DEV_PORT", default: 2992 },
// Using a built-in reverse proxy, the webpack dev assets are served from the
// same host and port as the app. In that case, the URLs to assets are relative
// without protocol, host, port.
// However, user can simulate CDN server with the proxy and have assets URLs
// specifying different host/port from the app. To do that, the following
// should be defined.
cdnProtocol: { env: ["WEBPACK_DEV_CDN_PROTOCOL"], type: "string", default: null },
cdnHostname: { env: ["WEBPACK_DEV_CDN_HOST"], type: "string", default: null },
cdnPort: { env: ["WEBPACK_DEV_CDN_PORT"], default: 0 },
//
// in dev mode, all webpack output are saved to memory only, but some files like
// stats.json are needed by different uses and the stats partial saves a copy to
// disk. It will use this as the path to save the file.
devArtifactsPath: { env: "WEBPACK_DEV_ARTIFACTS_PATH", default: ".etmp" },
cssModuleSupport: { env: "CSS_MODULE_SUPPORT", type: "boolean", default: undefined },
enableBabelPolyfill: { env: "ENABLE_BABEL_POLYFILL", default: false },
enableNodeSourcePlugin: { env: "ENABLE_NODESOURCE_PLUGIN", default: false },
enableHotModuleReload: { env: "WEBPACK_HOT_MODULE_RELOAD", default: true },
enableWarningsOverlay: { env: "WEBPACK_DEV_WARNINGS_OVERLAY", default: true },
woffFontInlineLimit: { env: "WOFF_FONT_INLINE_LIMIT", default: 1000 },
preserveSymlinks: {
env: ["WEBPACK_PRESERVE_SYMLINKS", "NODE_PRESERVE_SYMLINKS"],
default: false
},
enableShortenCSSNames: { env: "ENABLE_SHORTEN_CSS_NAMES", default: true },
minimizeSubappChunks: { env: "MINIMIZE_SUBAPP_CHUNKS", default: false },
optimizeCssOptions: {
env: "OPTIMIZE_CSS_OPTIONS",
type: "json",
default: {
cssProcessorOptions: {
zindex: false
}
}
},
loadDlls: { env: "ELECTRODE_LOAD_DLLS", type: "json", default: {} },
minify: { env: "WEBPACK_MINIFY", default: true }
};

module.exports = xenvConfig(webpackConfigSpec, userConfig.webpack, { merge });
7 changes: 7 additions & 0 deletions packages/xarc-app-dev/config/user-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use strict";

const Path = require("path");
const { merge } = require("lodash")

const optionalRequire = require("optional-require")(require);
module.exports = merge({ options: {} }, optionalRequire(Path.resolve("archetype/config")));
2 changes: 1 addition & 1 deletion packages/xarc-app-dev/lib/app-dev-middleware.js
Original file line number Diff line number Diff line change
@@ -59,7 +59,7 @@ class AppDevMiddleware {
}
});
// notify dev-admin that app server started
process.nextTick(() => process.send({ name: "app-setup" }));
process.nextTick(() => process.send && process.send({ name: "app-setup" }));
}
}

5 changes: 3 additions & 2 deletions packages/xarc-app-dev/lib/dev-admin/admin-server.js
Original file line number Diff line number Diff line change
@@ -16,13 +16,14 @@ const { fork } = require("child_process");
const ConsoleIO = require("./console-io");
const logger = require("@xarc/app/lib/logger");
const xaa = require("xaa");
const {
settings: { useDevProxy: DEV_PROXY_ENABLED }
} = require("../../config/dev-proxy");

const APP_SERVER_NAME = "your app server";
const DEV_SERVER_NAME = "Electrode webpack dev server";
const PROXY_SERVER_NAME = "Electrode Dev Proxy";

const DEV_PROXY_ENABLED = Boolean(process.env.APP_SERVER_PORT);

class AdminServer {
constructor(args, options) {
this._opts = args.opts;
8 changes: 4 additions & 4 deletions packages/xarc-app-dev/lib/dev-admin/log-parser.js
Original file line number Diff line number Diff line change
@@ -23,15 +23,15 @@ const BunyanLevelLookup = {
};
const parsers = [
{
custom: (raw) => raw.match(UnhandledRejection) ? [raw, "error", raw] : undefined,
custom: raw => (raw.match(UnhandledRejection) ? [raw, "error", raw] : undefined),
prefix: ""
},
{regex: LogParse, prefix: ""},
{ regex: LogParse, prefix: "" },
{
custom: (raw) => raw.match(NodeParse) ? [raw, "warn", raw] : undefined,
custom: raw => (raw.match(NodeParse) ? [raw, "warn", raw] : undefined),
prefix: NodeDebuggerTag
},
{regex: FyiLogParse, prefix: FyiTag}
{ regex: FyiLogParse, prefix: FyiTag }
];

function parseRegex(raw, parser) {
12 changes: 5 additions & 7 deletions packages/xarc-app-dev/lib/dev-admin/log-reader.js
Original file line number Diff line number Diff line change
@@ -42,12 +42,12 @@ const Levels = {
};

async function getLogsByLine(maxLevel = DefaultMaxLevel, handleLogLine) {
return new Promise((resolve) => {
return new Promise(resolve => {
const readInterface = readline.createInterface({
input: fs.createReadStream("archetype-debug.log")
});

readInterface.on("line", (event) => {
readInterface.on("line", event => {
event = JSON.parse(event);
const levelInfo = Levels[event.level];
if (levelInfo.index > maxLevel) {
@@ -61,15 +61,13 @@ async function getLogsByLine(maxLevel = DefaultMaxLevel, handleLogLine) {

async function getLogs(maxLevel = DefaultMaxLevel) {
const logs = [];
await getLogsByLine(maxLevel, (event) => logs.push(event));
await getLogsByLine(maxLevel, event => logs.push(event));
return logs;
}

function getLogEventAsAnsi(event) {
const levelInfo = Levels[event.level];
const name = levelInfo.color
? ck(`<${levelInfo.color}>${levelInfo.name}</>`)
: levelInfo.name;
const name = levelInfo.color ? ck(`<${levelInfo.color}>${levelInfo.name}</>`) : levelInfo.name;
return `${name}: ${event.message}`;
}

@@ -83,7 +81,7 @@ function getLogEventAsHtml(event) {

// eslint-disable-next-line no-console
async function displayLogs(maxLevel = DefaultMaxLevel, show = console.log) {
await getLogsByLine(maxLevel, (event) => show(getLogEventAsAnsi(event, show)));
await getLogsByLine(maxLevel, event => show(getLogEventAsAnsi(event, show)));
}

module.exports = {
15 changes: 6 additions & 9 deletions packages/xarc-app-dev/lib/dev-admin/middleware.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ const hotHelpers = require("webpack-hot-middleware/helpers");
const Url = require("url");
const { getWebpackStartConfig } = require("@xarc/webpack/lib/util/custom-check");
const { getLogs, getLogEventAsHtml } = require("./log-reader");
const { fullDevServer, controlPaths } = require("../../config/dev-proxy");
const { formUrl } = require("../utils");

hotHelpers.pathMatch = (url, path) => {
try {
@@ -23,12 +25,10 @@ const webpackDevMiddleware = require("webpack-dev-middleware");
const webpackHotMiddleware = require("webpack-hot-middleware");
const serveIndex = require("serve-index-fs");
const ck = require("chalker");
const archetype = require("@xarc/app/config/archetype");
const _ = require("lodash");
const statsUtils = require("../stats-utils");
const statsMapper = require("../stats-mapper");
const devRequire = archetype.devRequire;
const xsh = devRequire("xsh");
const xsh = require("xsh");
const shell = xsh.$;

function urlJoin() {
@@ -77,15 +77,12 @@ class Middleware {

const config = require(getWebpackStartConfig("webpack.config.dev.js"));

const { devPort, devHostname } = archetype.webpack;

// this is webpack-hot-middleware's default
this._hmrPath = "/__webpack_hmr";
this._hmrPath = controlPaths.hmr;

const webpackHotOptions = _.merge(
{
log: false,
path: `http://${devHostname}:${devPort}${this._hmrPath}`,
path: formUrl({ ...fullDevServer, path: this._hmrPath }),
heartbeat: 2000
},
options.hot
@@ -158,7 +155,7 @@ class Middleware {
});

this.cwdIndex = serveIndex(process.cwd(), { icons: true, hidden: true });
this.devBaseUrl = urlJoin(options.devBaseUrl || "/__electrode_dev");
this.devBaseUrl = urlJoin(options.devBaseUrl || controlPaths.dev);
this.devBaseUrlSlash = urlJoin(this.devBaseUrl, "/");
this.cwdBaseUrl = urlJoin(this.devBaseUrl, "/cwd");
this.cwdContextBaseUrl = urlJoin(this.devBaseUrl, "/memfs");
382 changes: 220 additions & 162 deletions packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js

Large diffs are not rendered by default.

125 changes: 88 additions & 37 deletions packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
"use strict";

/* eslint-disable no-magic-numbers, no-process-exit, global-require, no-console */
/* eslint-disable no-magic-numbers, no-process-exit, global-require, no-console, max-statements */

const sudoPrompt = require("sudo-prompt");
const request = require("request");
const http = require("http");
const Util = require("util");
const { controlPaths, settings, httpDevServer } = require("../../config/dev-proxy");
const proxyJs = require.resolve("./redbird-proxy");
const { formUrl } = require("../utils");

const canListenPort = async (port, host) => {
const server = http.createServer(() => {});

const getIntFromEnv = (name, defaultVal) => {
const envKey = [].concat(name).find(x => process.env[x]);
return parseInt(process.env[envKey] || (defaultVal && defaultVal.toString()), 10);
try {
await Util.promisify(server.listen.bind(server))(port, host);
} catch (err) {
if (err.code === "EACCES") {
return false;
}
} finally {
await Util.promisify(server.close.bind(server))();
}

return true;
};

const proxyJs = require.resolve("./redbird-proxy");
const isProxyRunning = async () => {
const { host, httpPort } = settings;

const port = getIntFromEnv("PORT", 3000);
const statusUrl = formUrl({
host,
port: httpPort,
path: controlPaths.status
});

const restartUrl = `http://localhost:${port}/__proxy_admin/restart`;
try {
await Util.promisify(request)(statusUrl);
return true;
} catch {
return false;
}
};

const handleRestart = type => {
const restart = () => {
console.log(`${type}Electrode dev proxy restarting`);
const restartUrl = formUrl({
...httpDevServer,
path: controlPaths.restart
});
request(restartUrl, (err, res, body) => {
if (!err) {
console.log(body);
} else {
console.error(body, err);
console.error("Restarting failed, body:", body, "Error", err, "\nrestart URL", restartUrl);
}
});
};
@@ -36,39 +67,59 @@ const handleRestart = type => {
});
};

if (port <= 1024) {
const exitUrl = `http://localhost:${port}/__proxy_admin/exit`;
async function mainSpawn() {
if (await isProxyRunning()) {
console.log("Electrode dev proxy already running - exiting.");
return;
}

const restart = () => {
sudoPrompt.exec(
`node ${proxyJs}`,
{
name: "Electrode Development Reverse Proxy"
},
(error, stdout, stderr) => {
console.log("stdout:", stdout);
if (error) {
console.error(error);
console.error("stderr:", stderr);
const { host, port, elevated } = settings;

let needElevated;

if (settings.port < 1024) {
// macOS mojave no longer need privileged access to listen on port < 1024
// https://news.ycombinator.com/item?id=18302380
// so run a simple listen on the port to check if it's needed
needElevated = !(await canListenPort(port));
}

if (elevated || needElevated) {
const exitUrl = formUrl({ host, port, path: controlPaths.exit });

const restart = () => {
sudoPrompt.exec(
`node ${proxyJs}`,
{
name: "Electrode Development Reverse Proxy"
},
(error, stdout, stderr) => {
console.log("stdout:", stdout);
if (error) {
console.error(error);
console.error("stderr:", stderr);
}
}
}
);
};
);
};

const handleElevatedProxy = () => {
process.on("SIGINT", () => {
request(exitUrl, () => {
console.log("Elevated Electrode dev proxy terminating");
process.nextTick(() => process.exit(0));
const handleElevatedProxy = () => {
process.on("SIGINT", () => {
request(exitUrl, () => {
console.log("Elevated Electrode dev proxy terminating");
process.nextTick(() => process.exit(0));
});
});
});
};
};

handleElevatedProxy();
handleRestart("Elevated ");
handleElevatedProxy();
handleRestart("Elevated ");

restart();
} else {
handleRestart("");
require("./redbird-proxy");
restart();
} else {
handleRestart("");
require("./redbird-proxy");
}
}

mainSpawn();
15 changes: 14 additions & 1 deletion packages/xarc-app-dev/lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
"use strict";
const Url = require("url");

const getOptRequire = require("@xarc/webpack/lib/util/get-opt-require");

const formUrl = ({ protocol = "http", host = "", port = "", path = "" }) => {
const proto = protocol.toString().toLowerCase();
const sp = port.toString();
const host2 =
host && port && !(sp === "80" && proto === "http") && !(sp === "443" && proto === "https")
? `${host}:${port}`
: host;

return Url.format({ protocol: proto, host: host2, pathname: path });
};

module.exports = {
getOptArchetypeRequire: getOptRequire
getOptArchetypeRequire: getOptRequire,
formUrl
};
40 changes: 26 additions & 14 deletions packages/xarc-app-dev/package.json
Original file line number Diff line number Diff line change
@@ -13,16 +13,16 @@
},
"license": "Apache-2.0",
"scripts": {
"test": "npm run lint && clap test",
"coverage": "npm run lint && clap check",
"lint": "eslint \"**/**/*.js\"",
"test": "clap test",
"coverage": "clap check",
"format": "prettier --write --print-width 100 *.{js,jsx} `find . -type d -d 1 -exec echo '{}/**/*.{js,jsx}' \\; | egrep -v '(/node_modules/|/dist/|/coverage/)'`"
},
"files": [
"config",
"dist",
"lib",
"scripts",
"require.js"
"require.js",
"scripts"
],
"author": "Electrode (http://www.electrode.io/)",
"contributors": [
@@ -44,9 +44,9 @@
"@babel/preset-env": "^7.1.6",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.0.0",
"@jchip/redbird": "^1.0.0",
"@jchip/redbird": "^1.1.0",
"@loadable/babel-plugin": "^5.10.0",
"@xarc/webpack": "../xarc-webpack",
"@xarc/webpack": "8.0.0",
"ansi-to-html": "^0.6.8",
"babel-plugin-dynamic-import-node": "^2.2.0",
"babel-plugin-lodash": "^3.3.4",
@@ -83,14 +83,15 @@
"webpack-hot-middleware": "^2.22.2",
"winston": "^2.3.1",
"xaa": "^1.2.2",
"xclap": "^0.2.38",
"xenv-config": "^1.3.0",
"xclap": "^0.2.45",
"xenv-config": "^1.3.1",
"xsh": "^0.4.4"
},
"devDependencies": {
"@xarc/app": "../xarc-app",
"@xarc/module-dev": "^2.0.3",
"babel-eslint": "^10.1.0",
"chai": "^4.0.0",
"electrode-archetype-njs-module-dev": "^3.0.0",
"electrode-archetype-opt-postcss": "../electrode-archetype-opt-postcss",
"electrode-archetype-opt-sass": "../electrode-archetype-opt-sass",
"electrode-archetype-opt-stylus": "../electrode-archetype-opt-stylus",
@@ -105,7 +106,9 @@
"fyn": {
"dependencies": {
"electrode-node-resolver": "../electrode-node-resolver",
"subapp-util": "../subapp-util"
"subapp-util": "../subapp-util",
"@jchip/redbird": "../../../redbird",
"@xarc/webpack": "../xarc-webpack"
},
"devDependencies": {
"@xarc/app": "../xarc-app",
@@ -120,21 +123,30 @@
"text-summary"
],
"exclude": [
"coverage",
"*clap.js",
"gulpfile.js",
"*clap.ts",
"coverage",
"dist",
"docs",
"gulpfile.js",
"test"
],
"check-coverage": false,
"statements": 0,
"branches": 0,
"functions": 0,
"lines": 0,
"cache": true
"cache": true,
"extends": []
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"mocha": {
"require": [
"@xarc/module-dev/config/test/setup.js"
],
"recursive": true
}
}
3 changes: 0 additions & 3 deletions packages/xarc-app-dev/test/.eslintrc

This file was deleted.

2 changes: 0 additions & 2 deletions packages/xarc-app-dev/test/mocha.opts

This file was deleted.

55 changes: 33 additions & 22 deletions packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js
Original file line number Diff line number Diff line change
@@ -19,10 +19,13 @@ describe("log-parser", function() {
});

it("should return the first level another level is detected midway through the message", () => {
const raw = "warn: An issue was detected in electrode error: Unable to load secrets file /secrets/ccm-secrets.json";
const raw =
"warn: An issue was detected in electrode error: Unable to load secrets file /secrets/ccm-secrets.json";
const { level, message } = parse(raw);
expect(level).equal("warn");
expect(message).equal("An issue was detected in electrode error: Unable to load secrets file /secrets/ccm-secrets.json");
expect(message).equal(
"An issue was detected in electrode error: Unable to load secrets file /secrets/ccm-secrets.json"
);
});

it("should detect Unhandled rejection messages as an error when they are annotated 'info'", () => {
@@ -33,7 +36,8 @@ describe("log-parser", function() {
});

it("should detect Unhandled rejection messages as an error when they are annotated 'debug'", () => {
const raw = "debug: Unhandled rejection (rejection id: 3): TypeError: Cannot read property 'Electrode' of undefined";
const raw =
"debug: Unhandled rejection (rejection id: 3): TypeError: Cannot read property 'Electrode' of undefined";
const { level, message } = parse(raw);
expect(level).equal("error");
expect(message).equal(raw);
@@ -55,13 +59,13 @@ describe("log-parser", function() {

it("should return error level and msg with badge for a node-bunyan level 50", () => {
const raw = JSON.stringify({
"name": "stdout",
"hostname": "localhost",
"pid": 131072,
"tags": ["error"],
"msg": "Electrode SOARI service discovery failed",
"level": 50,
"time": "2019-11-25T23:50:20.353Z"
name: "stdout",
hostname: "localhost",
pid: 131072,
tags: ["error"],
msg: "Electrode SOARI service discovery failed",
level: 50,
time: "2019-11-25T23:50:20.353Z"
});
const { level, message } = parse(raw);
expect(level).equal("error");
@@ -70,31 +74,36 @@ describe("log-parser", function() {

it("should return silly level and msg with badge for a node-bunyan level 10", () => {
const raw = JSON.stringify({
"name": "stdout",
"hostname": "localhost",
"pid": 131072,
"tags": ["silly"],
"msg": "The integers have been added together",
"level": 10,
"time": "2019-11-25T23:50:20.353Z"
name: "stdout",
hostname: "localhost",
pid: 131072,
tags: ["silly"],
msg: "The integers have been added together",
level: 10,
time: "2019-11-25T23:50:20.353Z"
});
const { level, message } = parse(raw);
expect(level).equal("silly");
expect(message).equal(`${BunyanTag}The integers have been added together`);
});

it("should return correct level and message with badge for an FYI warning and colon wrapped in color escape code", () => {
const raw = "\u001b[33mFYI warn:\u001b[39m electrode-ccm: Unable to load secrets file /secrets/ccm-secrets.json";
const raw =
"\u001b[33mFYI warn:\u001b[39m electrode-ccm: Unable to load secrets file /secrets/ccm-secrets.json";
const { level, message } = parse(raw);
expect(level).equal("warn");
expect(message).equal(`${FyiTag}electrode-ccm: Unable to load secrets file /secrets/ccm-secrets.json`);
expect(message).equal(
`${FyiTag}electrode-ccm: Unable to load secrets file /secrets/ccm-secrets.json`
);
});

it("should preserve color escape codes in message but not in level", () => {
const raw = "\u001b[33msilly\u001b[39m: Electrode discoverWithSearch - \u001b[35mdiscovering\u001b[39m {\"environment\": \"qa\"}";
const raw = `\u001b[33msilly\u001b[39m: Electrode discoverWithSearch - \u001b[35mdiscovering\u001b[39m {"environment": "qa"}`;
const { level, message } = parse(raw);
expect(level).equal("silly");
expect(message).equal("Electrode discoverWithSearch - \u001b[35mdiscovering\u001b[39m {\"environment\": \"qa\"}");
expect(message).equal(
`Electrode discoverWithSearch - \u001b[35mdiscovering\u001b[39m {"environment": "qa"}`
);
});

it("should return info for level and raw message for message if the log line has an unknown level", () => {
@@ -115,7 +124,9 @@ describe("log-parser", function() {
const raw = "Debugger listening on ws://127.0.0.1:9229/75cf1993-daff-4533-a53e-30fb92a5ad16";
const { level, message } = parse(raw);
expect(level).equal("warn");
expect(message).equal("[nod] Debugger listening on ws://127.0.0.1:9229/75cf1993-daff-4533-a53e-30fb92a5ad16");
expect(message).equal(
"[nod] Debugger listening on ws://127.0.0.1:9229/75cf1993-daff-4533-a53e-30fb92a5ad16"
);
});

it("should mark the inspector help message as a 'warn' so it is rendered to the console", () => {
2 changes: 1 addition & 1 deletion packages/xarc-app-dev/xclap.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
require("electrode-archetype-njs-module-dev")();
require("@xarc/module-dev")();
10 changes: 10 additions & 0 deletions packages/xarc-app/arch-clap.js
Original file line number Diff line number Diff line change
@@ -873,6 +873,16 @@ Individual .babelrc files were generated for you in src/client and src/server
}
},

"dev-proxy": {
desc:
"Start Electrode dev reverse proxy by itself - useful for running it with sudo (options: --debug)",
task() {
const debug = this.argv.includes("--debug") ? "--inspect-brk " : "";
const proxySpawn = require.resolve("@xarc/app-dev/lib/dev-admin/redbird-spawn");
return `~(tty)$node ${debug}${proxySpawn}`;
}
},

"test-server": xclap.concurrent(["lint-server", "lint-server-test"], "test-server-cov"),
"test-watch-all": xclap.concurrent("server-admin.test", "test-frontend-dev-watch"),

0 comments on commit 6aaed71

Please sign in to comment.