diff --git a/docs/guides/local-https-setup.md b/docs/guides/local-https-setup.md new file mode 100644 index 000000000..378b4c15f --- /dev/null +++ b/docs/guides/local-https-setup.md @@ -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 diff --git a/packages/subapp-server/lib/setup-hapi-routes.js b/packages/subapp-server/lib/setup-hapi-routes.js index bc3636332..f6f8440ff 100644 --- a/packages/subapp-server/lib/setup-hapi-routes.js +++ b/packages/subapp-server/lib/setup-hapi-routes.js @@ -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 }); diff --git a/packages/subapp-server/lib/utils.js b/packages/subapp-server/lib/utils.js index 6349be871..3b0ab1b9e 100644 --- a/packages/subapp-server/lib/utils.js +++ b/packages/subapp-server/lib/utils.js @@ -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") diff --git a/packages/subapp-util/lib/index.js b/packages/subapp-util/lib/index.js index 64b48a966..1e8156200 100644 --- a/packages/subapp-util/lib/index.js +++ b/packages/subapp-util/lib/index.js @@ -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 }; diff --git a/packages/subapp-web/lib/load.js b/packages/subapp-web/lib/load.js index 50bf5d16f..71493a6ac 100644 --- a/packages/subapp-web/lib/load.js +++ b/packages/subapp-web/lib/load.js @@ -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 }); } diff --git a/packages/xarc-app-dev/.eslintrc b/packages/xarc-app-dev/.eslintrc deleted file mode 100644 index 8d5eed087..000000000 --- a/packages/xarc-app-dev/.eslintrc +++ /dev/null @@ -1,30 +0,0 @@ ---- -extends: - - walmart/configurations/es6-node -parserOptions: - sourceType: module -rules: - strict: - - 'off' - - global - no-process-exit: - - 'off' - func-style: - - 'off' - global-require: - - 'off' - no-magic-numbers: - - 'off' - no-console: - - 'off' - quotes: - - error - - double - - avoidEscape: true - allowTemplateLiterals: true - max-len: - - 2 - - 120 - no-extra-parens: [0] - prefer-spread: - - 'off' diff --git a/packages/xarc-app-dev/config/archetype.js b/packages/xarc-app-dev/config/archetype.js index cc4d3ca46..3121f75c3 100644 --- a/packages/xarc-app-dev/config/archetype.js +++ b/packages/xarc-app-dev/config/archetype.js @@ -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 }) diff --git a/packages/xarc-app-dev/config/dev-proxy.js b/packages/xarc-app-dev/config/dev-proxy.js new file mode 100644 index 000000000..e3b9709f4 --- /dev/null +++ b/packages/xarc-app-dev/config/dev-proxy.js @@ -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() +}; diff --git a/packages/xarc-app-dev/config/env-app.js b/packages/xarc-app-dev/config/env-app.js new file mode 100644 index 000000000..69786bc24 --- /dev/null +++ b/packages/xarc-app-dev/config/env-app.js @@ -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 }); diff --git a/packages/xarc-app-dev/config/env-babel.js b/packages/xarc-app-dev/config/env-babel.js new file mode 100644 index 000000000..c56d09a05 --- /dev/null +++ b/packages/xarc-app-dev/config/env-babel.js @@ -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 }); diff --git a/packages/xarc-app-dev/config/env-karma.js b/packages/xarc-app-dev/config/env-karma.js new file mode 100644 index 000000000..0cf63d51c --- /dev/null +++ b/packages/xarc-app-dev/config/env-karma.js @@ -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 }); diff --git a/packages/xarc-app-dev/config/env-proxy.js b/packages/xarc-app-dev/config/env-proxy.js new file mode 100644 index 000000000..c6f0833fb --- /dev/null +++ b/packages/xarc-app-dev/config/env-proxy.js @@ -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 }); diff --git a/packages/xarc-app-dev/config/env-webpack.js b/packages/xarc-app-dev/config/env-webpack.js new file mode 100644 index 000000000..46d7f2e30 --- /dev/null +++ b/packages/xarc-app-dev/config/env-webpack.js @@ -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 }); diff --git a/packages/xarc-app-dev/config/user-config.js b/packages/xarc-app-dev/config/user-config.js new file mode 100644 index 000000000..fbcb1cbd6 --- /dev/null +++ b/packages/xarc-app-dev/config/user-config.js @@ -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"))); diff --git a/packages/xarc-app-dev/lib/app-dev-middleware.js b/packages/xarc-app-dev/lib/app-dev-middleware.js index 5a8d66db3..a3de75c2c 100644 --- a/packages/xarc-app-dev/lib/app-dev-middleware.js +++ b/packages/xarc-app-dev/lib/app-dev-middleware.js @@ -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" })); } } diff --git a/packages/xarc-app-dev/lib/dev-admin/admin-server.js b/packages/xarc-app-dev/lib/dev-admin/admin-server.js index c6889824d..7b1c38484 100644 --- a/packages/xarc-app-dev/lib/dev-admin/admin-server.js +++ b/packages/xarc-app-dev/lib/dev-admin/admin-server.js @@ -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; diff --git a/packages/xarc-app-dev/lib/dev-admin/log-parser.js b/packages/xarc-app-dev/lib/dev-admin/log-parser.js index 16e9a2af8..5c9d76506 100644 --- a/packages/xarc-app-dev/lib/dev-admin/log-parser.js +++ b/packages/xarc-app-dev/lib/dev-admin/log-parser.js @@ -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) { diff --git a/packages/xarc-app-dev/lib/dev-admin/log-reader.js b/packages/xarc-app-dev/lib/dev-admin/log-reader.js index 9a81d399f..4616b1fe8 100644 --- a/packages/xarc-app-dev/lib/dev-admin/log-reader.js +++ b/packages/xarc-app-dev/lib/dev-admin/log-reader.js @@ -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 = { diff --git a/packages/xarc-app-dev/lib/dev-admin/middleware.js b/packages/xarc-app-dev/lib/dev-admin/middleware.js index 452b459a0..1d43f6bdd 100644 --- a/packages/xarc-app-dev/lib/dev-admin/middleware.js +++ b/packages/xarc-app-dev/lib/dev-admin/middleware.js @@ -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"); diff --git a/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js b/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js index 6fad7fe6a..360295e38 100644 --- a/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js +++ b/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js @@ -2,229 +2,287 @@ /* eslint-disable max-statements, no-process-exit, global-require, no-console */ -const Url = require("url"); +const assert = require("assert"); const Path = require("path"); const _ = require("lodash"); const redbird = require("@jchip/redbird"); const ck = require("chalker"); const optionalRequire = require("optional-require")(require); +const { settings, certs: proxyCerts, controlPaths } = require("../../config/dev-proxy"); -const getIntFromEnv = (name, defaultVal) => { - const envKey = [].concat(name).find(x => process.env[x]); - return parseInt(process.env[envKey] || (defaultVal && defaultVal.toString()), 10); -}; +const { formUrl } = require("../utils"); -const getHost = () => { - return process.env.HOST || "localhost"; -}; +let APP_RULES = []; const getNotFoundPage = data => { - const { actualHost, expectedHost, port } = data; + const { protocol, actualHost, expectedHost, port } = data; + const adminUrl = formUrl({ protocol, host: expectedHost, port, path: controlPaths.status }); /* eslint-disable max-len */ - const invalidHostMessage = ` - <p> - <span style="color: red;">ERROR:</span> You used a hostname that the development proxy doesn't recognize.<br /> - The proxy is using the hostname <span style="color: green;">${expectedHost}</span>.<br /> - The hostname in your URL is <span style="color: red;">${actualHost}</span>.<br /> - To change the hostname that the proxy uses, set the HOST env variable.<br /> - For example:<br /><br /> - In unix bash: <code style="color: white;background-color: black;padding: 5px;"> - HOST=${actualHost} clap dev - </code><br /><br /> - In Windows PowerShell: <code style="color: white;background-color: #012456;padding: 5px;"> - $Env:HOST="${actualHost}"; clap dev - </code> - </p> - `; - return ` - <html> - <body> - <div style="text-align:center;"> - <div> - <svg version="1.0" viewBox="100 0 202.3 65.1" width="250px" height="250px"> - <g transform="scale(5,5) translate(-94, -10)"> - <path id="Fill-1" class="st0" d="M134.6,8.9c-0.2,0-0.3-0.1-0.4-0.2l-1.1-1.9l-0.9,1.8c-0.1,0.2-0.4,0.3-0.6,0.2 c-0.1-0.1-0.2-0.2-0.2-0.3l-0.8-4.6l-0.9,2.7c-0.1,0.2-0.2,0.3-0.4,0.3h-2.2c-0.2,0-0.4-0.2-0.4-0.4c0-0.2,0.2-0.4,0.4-0.4 h1.9l1.3-4.1c0.1-0.2,0.3-0.3,0.5-0.3c0.1,0,0.3,0.2,0.3,0.3l0.9,5.1l0.7-1.3c0.1-0.2,0.4-0.3,0.6-0.2c0.1,0,0.1,0.1,0.2,0.2 l1,1.6l1.6-7c0.1-0.2,0.3-0.4,0.5-0.3c0.2,0,0.3,0.2,0.3,0.3l1,5.7l3.4,0.2c0.2,0,0.4,0.2,0.4,0.4c0,0.2-0.2,0.4-0.4,0.4l0,0 l-3.7-0.2c-0.2,0-0.4-0.2-0.4-0.3l-0.8-4L135,8.6C135,8.7,134.8,8.9,134.6,8.9L134.6,8.9"></path> - <path d="M134.3,31.9 c-4.9,0 -8.8,-3.9 -8.8,-8.8 s3.9,-8.8 8.8,-8.8 s8.8,3.9 8.8,8.8 S139.2,31.9 134.3,31.9 L134.3,31.9 M145.1,18.5 h-0.3 l-0.1,-0.3 c-0.6,-1.3 -1.5,-2.4 -2.5,-3.4 l-0.4,-0.3 l3.4,-5.8 h0.3 c1,-0.2 1.6,-1.1 1.5,-2 s-1.1,-1.6 -2,-1.5 s-1.6,1.1 -1.5,2 c0,0.2 0.1,0.4 0.2,0.6 l0.2,0.3 l-3.3,5.4 l-0.5,-0.3 c-1.7,-0.9 -3.6,-1.5 -5.6,-1.5 l0,0 c-1.9,0 -3.9,0.5 -5.6,1.5 l-0.5,0.3 L124.9,8 l0.1,-0.3 c0.1,-0.2 0.2,-0.5 0.2,-0.8 c0,-1 -0.8,-1.8 -1.8,-1.8 s-1.8,0.8 -1.8,1.8 c0,0.9 0.7,1.6 1.5,1.7 h0.3 l3.4,5.8 l-0.4,0.3 c-1,0.9 -1.9,2.1 -2.5,3.3 l-0.2,0.3 h-0.3 c-2.4,0.2 -4.2,2.4 -4,4.8 c0.2,2 1.7,3.7 3.8,4 h0.3 l0.1,0.3 c1.8,4.2 5.9,7 10.5,7 l0,0 c4.6,0 8.7,-2.8 10.5,-7 l0.1,-0.3 h0.3 c2.4,-0.4 4.1,-2.6 3.7,-5 C148.7,20.2 147.1,18.6 145.1,18.5 " id="Fill-4" class="st0"></path> - <path id="Fill-7" class="st0" d="M138.9,21.1c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3 S138.9,20.4,138.9,21.1C138.9,21.1,138.9,21.1,138.9,21.1"></path> - <path id="Fill-9" class="st0" d="M132.2,21.1c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3c0,0,0,0,0,0 C131.7,19.8,132.3,20.4,132.2,21.1C132.3,21.1,132.3,21.1,132.2,21.1"></path> - </g> - </svg> - <p> - Electrode development reverse proxy is unable to forward your URL.<br /> - Check <a href="http://${expectedHost}:${port}/__proxy_admin/status"> - http://${expectedHost}:${port}/__proxy_admin/status - </a> to see a list of forward rules. - </p> - ${actualHost !== expectedHost ? invalidHostMessage : ""} - </div> - </div> - </body> - </html> - `; + const invalidHostMessage = `<p> + <span style="color: red;">ERROR:</span> You used a hostname that the development proxy doesn't recognize.<br /> + The proxy is using the hostname <span style="color: green;">${expectedHost}</span>.<br /> + The hostname in your URL is <span style="color: red;">${actualHost}</span>.<br /> + To change the hostname that the proxy uses, set the HOST env variable.<br /> + For example:<br /><br /> + In unix bash: <code style="color: white;background-color: black;padding: 5px;"> + HOST=${actualHost} clap dev + </code><br /><br /> + In Windows PowerShell: <code style="color: white;background-color: #012456;padding: 5px;"> + $Env:HOST="${actualHost}"; clap dev + </code> +</p>`; + return `<html> + <body> + <div style="text-align:center;"> + <div> + <svg version="1.0" viewBox="100 0 202.3 65.1" width="250px" height="250px"> + <g transform="scale(5,5) translate(-94, -10)"> + <path id="Fill-1" class="st0" d="M134.6,8.9c-0.2,0-0.3-0.1-0.4-0.2l-1.1-1.9l-0.9,1.8c-0.1,0.2-0.4,0.3-0.6,0.2 c-0.1-0.1-0.2-0.2-0.2-0.3l-0.8-4.6l-0.9,2.7c-0.1,0.2-0.2,0.3-0.4,0.3h-2.2c-0.2,0-0.4-0.2-0.4-0.4c0-0.2,0.2-0.4,0.4-0.4 h1.9l1.3-4.1c0.1-0.2,0.3-0.3,0.5-0.3c0.1,0,0.3,0.2,0.3,0.3l0.9,5.1l0.7-1.3c0.1-0.2,0.4-0.3,0.6-0.2c0.1,0,0.1,0.1,0.2,0.2 l1,1.6l1.6-7c0.1-0.2,0.3-0.4,0.5-0.3c0.2,0,0.3,0.2,0.3,0.3l1,5.7l3.4,0.2c0.2,0,0.4,0.2,0.4,0.4c0,0.2-0.2,0.4-0.4,0.4l0,0 l-3.7-0.2c-0.2,0-0.4-0.2-0.4-0.3l-0.8-4L135,8.6C135,8.7,134.8,8.9,134.6,8.9L134.6,8.9"></path> + <path d="M134.3,31.9 c-4.9,0 -8.8,-3.9 -8.8,-8.8 s3.9,-8.8 8.8,-8.8 s8.8,3.9 8.8,8.8 S139.2,31.9 134.3,31.9 L134.3,31.9 M145.1,18.5 h-0.3 l-0.1,-0.3 c-0.6,-1.3 -1.5,-2.4 -2.5,-3.4 l-0.4,-0.3 l3.4,-5.8 h0.3 c1,-0.2 1.6,-1.1 1.5,-2 s-1.1,-1.6 -2,-1.5 s-1.6,1.1 -1.5,2 c0,0.2 0.1,0.4 0.2,0.6 l0.2,0.3 l-3.3,5.4 l-0.5,-0.3 c-1.7,-0.9 -3.6,-1.5 -5.6,-1.5 l0,0 c-1.9,0 -3.9,0.5 -5.6,1.5 l-0.5,0.3 L124.9,8 l0.1,-0.3 c0.1,-0.2 0.2,-0.5 0.2,-0.8 c0,-1 -0.8,-1.8 -1.8,-1.8 s-1.8,0.8 -1.8,1.8 c0,0.9 0.7,1.6 1.5,1.7 h0.3 l3.4,5.8 l-0.4,0.3 c-1,0.9 -1.9,2.1 -2.5,3.3 l-0.2,0.3 h-0.3 c-2.4,0.2 -4.2,2.4 -4,4.8 c0.2,2 1.7,3.7 3.8,4 h0.3 l0.1,0.3 c1.8,4.2 5.9,7 10.5,7 l0,0 c4.6,0 8.7,-2.8 10.5,-7 l0.1,-0.3 h0.3 c2.4,-0.4 4.1,-2.6 3.7,-5 C148.7,20.2 147.1,18.6 145.1,18.5 " id="Fill-4" class="st0"></path> + <path id="Fill-7" class="st0" d="M138.9,21.1c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3 S138.9,20.4,138.9,21.1C138.9,21.1,138.9,21.1,138.9,21.1"></path> + <path id="Fill-9" class="st0" d="M132.2,21.1c0,0.7-0.6,1.3-1.3,1.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3c0,0,0,0,0,0 C131.7,19.8,132.3,20.4,132.2,21.1C132.3,21.1,132.3,21.1,132.2,21.1"></path> + </g> + </svg> + <p> + Electrode development reverse proxy is unable to forward your URL.<br /> + Check <a href="${adminUrl}">${adminUrl}</a> to see a list of forward rules. + </p> + ${actualHost !== expectedHost ? invalidHostMessage : ""} + </div> + </div> + </body> +</html>`; /* eslint-enable max-len */ }; // eslint-disable-next-line max-params -const makeUrl = (host, port, pathname = "", protocol = "http") => { - // eslint-disable-next-line - if (port !== 80) { - host = `${host}:${port}`; - } - return Url.format({ - protocol, +const getLineForTree = (protocol, host) => (tree, [envVarName, port], idx, arr) => { + const boxChar = idx === arr.length - 1 ? "└" : "├"; + const portText = envVarName.replace(/port/gi, ""); + return ck`${tree} ${boxChar}─<green>${formUrl({ + protocol: "http", host, - port: `${port}`, - pathname - }); + port + })}</> (${portText})\n`; }; -// eslint-disable-next-line max-params -const getLineForTree = (host) => (tree, [envVarName, port], idx, arr) => { - const boxChar = idx === arr.length - 1 ? "└" : "├"; - - return `${tree} ${boxChar}─http://${host}:${port} (${envVarName.replace(/port/ig, "")})\n`; -}; +const buildProxyTree = (options, ports) => { + const { host, port, protocol } = options; -const buildProxyTree = (options) => { - const {host, port} = options; - const portTree = Object.entries(options) - .filter(([k]) => k !== "port" && k.match(/port/ig)) - .reduce(getLineForTree(host), ""); + const lineGen = getLineForTree(protocol, host); + const portTree = Object.entries(_.pick(options, ports)).reduce(lineGen, ""); - return `http://${host}:${port} (proxy) \n${portTree}`; + return ck`<orange>${formUrl({ protocol, host, port })}</> (proxy) \n${portTree}`; }; -const startProxy = options => { - options = Object.assign( - { - host: getHost(), - port: getIntFromEnv("PORT", "3000"), - appPort: getIntFromEnv("APP_SERVER_PORT", "3001"), - webpackDevPort: getIntFromEnv(["WEBPACK_PORT", "WEBPACK_DEV_PORT"], "2992"), - xfwd: false, - bunyan: { - level: "warn", - name: "redbird" - }, - resolvers: [] - }, - options - ); - - const proxyOptions = _.pick(options, ["port", "xfwd", "bunyan", "resolvers"]); - const { host, port } = options; - - let proxy = redbird(proxyOptions); - - proxy.notFound((req, res) => { - res.statusCode = 404; - res.setHeader("Content-Type", "text/html"); - res.write( - getNotFoundPage({ - actualHost: req.headers.host.split(":")[0], - expectedHost: getHost(), - port: getIntFromEnv("PORT", "3000") - }) - ); - res.end(); - }); - - const { appPort, webpackDevPort } = options; - - const rules = [ - [makeUrl(host, port), makeUrl(host, appPort)], - [makeUrl(host, port, `/js`), makeUrl(host, webpackDevPort, `/js`)], - [makeUrl(host, port, `/__electrode_dev`), makeUrl(host, webpackDevPort, `/__electrode_dev`)], - [makeUrl("127.0.0.1", port), makeUrl(host, appPort)], - [makeUrl(`127.0.0.1`, port, `/js`), makeUrl(host, webpackDevPort, `/js`)], - [ - makeUrl(`127.0.0.1`, port, `/__electrode_dev`), - makeUrl(host, webpackDevPort, `/__electrode_dev`) - ] - ]; - - rules.forEach(x => proxy.register(...x)); - - proxy.register({ - src: `${host}/__proxy_admin/exit`, - target: `http://localhost:29999/skip`, - onRequest: (req, res) => { - res.statusCode = 200; - res.write(`<!DOCTYPE html> +const onExitRequest = (req, res) => { + res.statusCode = 200; + res.write(`<!DOCTYPE html> <html><head></head><body> <h1>Bye</h1> </body> </html>`); - res.end(); - process.nextTick(() => process.exit(0)); + res.end(); + process.nextTick(() => process.exit(0)); - return false; - } - }); + return false; +}; - proxy.register({ - src: `${host}/__proxy_admin/status`, - target: `http://localhost:29999/skip`, - onRequest: (req, res) => { - res.statusCode = 200; - res.write(`<!DOCTYPE html> +const onStatusRequest = (req, res) => { + res.statusCode = 200; + res.write(`<!DOCTYPE html> <html><head></head><body> <h1>Electrode Development Reverse Proxy</h1> <h2>Rules</h2> <ul> -${rules - .map( - x => `<li><pre><a href="${x[0]}">${x[0]}</a> ===> <a href="${x[1]}">${x[1]}</a></pre></li>` - ) - .join("")} +${APP_RULES.map( + x => `<li><pre><a href="${x[0]}">${x[0]}</a> ===> <a href="${x[1]}">${x[1]}</a></pre></li>` +).join("")} </ul> </body> </html>`); - res.end(); + res.end(); - return false; - } - }); + return false; +}; - const restart = () => { - proxy.close(); - proxy = undefined; - process.nextTick(startProxy); - }; +const registerElectrodeDevRules = ({ + proxy, + ssl, + protocol, + host, + port, + appPort, + webpackDevPort, + restart +}) => { + const { dev: devPath, admin: adminPath, hmr: hmrPath, appLog, reporter } = controlPaths; + const appForwards = [ + [{}, { port: appPort }], + [{ path: `/js` }, { path: `/js`, port: webpackDevPort }], + [{ path: devPath }, { path: devPath, port: webpackDevPort }], + [{ path: hmrPath }, { path: hmrPath, port: webpackDevPort }], + [{ path: appLog }, { path: appLog, port: webpackDevPort }], + [{ path: reporter }, { path: reporter, port: webpackDevPort }], + [{ path: `${adminPath}/test-google` }, { protocol: "https", host: "www.google.com" }] + ]; + const appRules = appForwards + .map(([src, target, opts]) => { + return [ + formUrl({ host, protocol, port, ...src }), + formUrl({ host, protocol: "http", ...target }), + opts + ]; + }) + .concat( + // repeat all rules for 127.0.0.1 + appForwards.map(([src, target, opts]) => { + return [ + formUrl({ protocol, port, ...src, host: src.host || "127.0.0.1" }), + formUrl({ + ...target, + protocol: target.protocol || "http", + host: target.host || "127.0.0.1" + }), + opts + ]; + }) + ) + .filter(x => x); - const userDevProxyFile = optionalRequire.resolve(Path.resolve("archetype/config/dev-proxy")); + appRules.forEach(x => proxy.register(...x)); - const userDevProxy = userDevProxyFile && require(userDevProxyFile); + APP_RULES = APP_RULES.concat(appRules); + + proxy.register({ + ssl, + src: formUrl({ protocol, host, path: controlPaths.exit }), + target: `http://localhost:29999/skip`, + onRequest: onExitRequest + }); proxy.register({ - src: `${host}/__proxy_admin/restart`, + ssl, + src: formUrl({ protocol, host, path: controlPaths.status }), + target: `http://localhost:29999/skip`, + onRequest: onStatusRequest + }); + + proxy.register({ + ssl, + src: formUrl({ protocol, host, path: controlPaths.restart }), target: `http://localhost:29999/skip`, onRequest: (req, res) => { res.statusCode = 200; res.write(`restarted`); res.end(); - // ensure user's proxy rules are refreshed - delete require.cache[userDevProxyFile]; - process.nextTick(restart); return false; } }); +}; + +const startProxy = inOptions => { + APP_RULES = []; + const options = Object.assign( + { + xfwd: false, + bunyan: { + level: "warn", + name: "redbird" + }, + resolvers: [] + }, + settings, + inOptions + ); + + const proxyOptions = _.pick(options, ["port", "xfwd", "bunyan", "resolvers"]); + const { host, port, protocol } = options; + + const ssl = Boolean(options.httpsPort); + + if (ssl) { + assert(proxyCerts.key, "Dev Proxy can't find SSL key and certs"); + Object.assign(proxyOptions, { + // We still setup a regular http rules even if HTTPS is enabled + port: options.httpPort, + host, + secure: true, + ssl: { + port: options.httpsPort, + ...proxyCerts + } + }); + } + + let proxy = redbird(proxyOptions); + + const userFiles = ["archetype/config", "src", "test", "config"].map(x => `${x}/dev-proxy-rules`); + const userDevProxyFile = userFiles.find(f => optionalRequire.resolve(Path.resolve(f))); + + const userDevProxy = userDevProxyFile && require(userDevProxyFile); + + const restart = async () => { + // ensure user's proxy rules are refreshed + if (userDevProxyFile) { + delete require.cache[userDevProxyFile]; + } + if (proxy) { + console.log("... Closing proxy server ..."); + const old = proxy; + proxy = undefined; + await old.close(true); + process.nextTick(() => startProxy(inOptions)); + } + }; + + proxy.notFound((req, res) => { + res.statusCode = 404; + res.setHeader("Content-Type", "text/html"); + res.write( + getNotFoundPage({ + protocol, + actualHost: req.headers.host.split(":")[0], + expectedHost: host, + port + }) + ); + res.end(); + }); + + // register with primary protocol/host/port + registerElectrodeDevRules({ ...options, ssl, proxy, restart }); + + // if primary protocol is https, then register regular http rules at httpPort + if (ssl) { + registerElectrodeDevRules({ + proxy, + protocol: "http", + host, + port: options.httpPort, + appPort: options.appPort, + webpackDevPort: options.webpackDevPort, + restart + }); + } if (userDevProxy && userDevProxy.setupRules) { console.log("Calling proxy setupRules from your archetype/config/dev-proxy"); userDevProxy.setupRules(proxy, options); } + const proxyUrl = formUrl({ protocol, host, port: options.port }); console.log( ck`Electrode dev proxy server running: -${buildProxyTree(options)} -View status at <green>http://${host}:${options.port}/__proxy_admin/status</>`); - console.log(ck`You can access your app at <green>http://${host}:${options.port}</>`); +${buildProxyTree(options, ["appPort", "webpackDevPort"])} +View status at <green>${proxyUrl}${controlPaths.status}</>` + ); + console.log(ck`You can access your app at <green>${proxyUrl}</>`); }; module.exports = startProxy; diff --git a/packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js b/packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js index a01fbaac7..cd7187a0b 100644 --- a/packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js +++ b/packages/xarc-app-dev/lib/dev-admin/redbird-spawn.js @@ -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(); diff --git a/packages/xarc-app-dev/lib/utils.js b/packages/xarc-app-dev/lib/utils.js index 5073ba878..f1d54151d 100644 --- a/packages/xarc-app-dev/lib/utils.js +++ b/packages/xarc-app-dev/lib/utils.js @@ -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 }; diff --git a/packages/xarc-app-dev/package.json b/packages/xarc-app-dev/package.json index c0fbbbd48..665574252 100644 --- a/packages/xarc-app-dev/package.json +++ b/packages/xarc-app-dev/package.json @@ -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,10 +123,12 @@ "text-summary" ], "exclude": [ - "coverage", "*clap.js", - "gulpfile.js", + "*clap.ts", + "coverage", "dist", + "docs", + "gulpfile.js", "test" ], "check-coverage": false, @@ -131,10 +136,17 @@ "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 } } diff --git a/packages/xarc-app-dev/test/.eslintrc b/packages/xarc-app-dev/test/.eslintrc deleted file mode 100644 index a4edd1e02..000000000 --- a/packages/xarc-app-dev/test/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ ---- -extends: - - "../node_modules/electrode-archetype-njs-module-dev/config/eslint/.eslintrc-test" diff --git a/packages/xarc-app-dev/test/mocha.opts b/packages/xarc-app-dev/test/mocha.opts deleted file mode 100644 index 022f99b50..000000000 --- a/packages/xarc-app-dev/test/mocha.opts +++ /dev/null @@ -1,2 +0,0 @@ ---require node_modules/electrode-archetype-njs-module-dev/config/test/setup.js ---recursive diff --git a/packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js b/packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js index b55d4fd72..d40982cd2 100644 --- a/packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js +++ b/packages/xarc-app-dev/test/spec/dev-admin/log-parser.spec.js @@ -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,13 +74,13 @@ 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"); @@ -84,17 +88,22 @@ describe("log-parser", function() { }); 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", () => { diff --git a/packages/xarc-app-dev/xclap.js b/packages/xarc-app-dev/xclap.js index ea371779c..65b0c2da6 100644 --- a/packages/xarc-app-dev/xclap.js +++ b/packages/xarc-app-dev/xclap.js @@ -1 +1 @@ -require("electrode-archetype-njs-module-dev")(); +require("@xarc/module-dev")(); diff --git a/packages/xarc-app/arch-clap.js b/packages/xarc-app/arch-clap.js index cf357beac..831487ff1 100644 --- a/packages/xarc-app/arch-clap.js +++ b/packages/xarc-app/arch-clap.js @@ -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"),