From 638ed9b718082457084cdcdcb8aea286c6679d86 Mon Sep 17 00:00:00 2001 From: Joel Chen Date: Mon, 27 Apr 2020 10:21:39 -0700 Subject: [PATCH] feat: support mock CDN in dev-proxy for mock prod mode (#1627) * feat: support mock CDN in dev-proxy for mock prod mode * back to mime 1 --- .../xarc-app-dev/lib/dev-admin/cdn-mock.js | 75 ++++++++++++++++ .../lib/dev-admin/redbird-proxy.js | 29 ++++++- packages/xarc-app-dev/package.json | 15 ++-- packages/xarc-app/arch-clap.js | 87 +++++++++++++++++-- 4 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 packages/xarc-app-dev/lib/dev-admin/cdn-mock.js diff --git a/packages/xarc-app-dev/lib/dev-admin/cdn-mock.js b/packages/xarc-app-dev/lib/dev-admin/cdn-mock.js new file mode 100644 index 000000000..0016d94fb --- /dev/null +++ b/packages/xarc-app-dev/lib/dev-admin/cdn-mock.js @@ -0,0 +1,75 @@ +"use strict"; + +/* eslint-disable no-console, no-magic-numbers, prefer-template */ + +/* + * search all files under dist and generate a config/assets.json file for mocking CDN + */ + +const Path = require("path"); +const filterScanDir = require("filter-scan-dir"); +const Url = require("url"); +const Fs = require("fs"); +const chokidar = require("chokidar"); +const mime = require("mime"); +const LOADED_ASSETS = {}; + +const cdnMock = { + generateMockAssets(baseUrl) { + const watcher = chokidar.watch("dist"); + let timer; + const updateCdnMock = path => { + LOADED_ASSETS[path] = undefined; + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + timer = undefined; + console.log("Refreshing mock CDN mapping - please restart app"); + cdnMock._generateMockAssets(baseUrl); + }, 250).unref(); + }; + watcher.on("change", updateCdnMock); + cdnMock._generateMockAssets(baseUrl); + }, + + _generateMockAssets(baseUrl) { + const files = filterScanDir.sync({ dir: "dist" }); + + const url = Url.parse(baseUrl); + const noProtocolBase = Path.posix.join(`/`, url.host, url.path); + const timestamp = Math.floor(Date.now() / 1000); + + const mockAssets = files.reduce((acc, file) => { + acc[Path.basename(file)] = "/" + Path.posix.join(noProtocolBase, `${timestamp}`, file); + return acc; + }, {}); + + Fs.writeFileSync("config/assets.json", `${JSON.stringify(mockAssets, null, 2)}\n`); + }, + + respondAsset(req, res) { + try { + const filePath = req.url.replace(/\/__mock-cdn\/[0-9]+/, "dist"); + const fp = Path.resolve(filePath); + let asset = LOADED_ASSETS[filePath]; + if (!asset) { + asset = LOADED_ASSETS[filePath] = Fs.readFileSync(fp); + } + const ext = Path.extname(filePath); + const mimeType = mime.getType(ext); + res.writeHead(200, { + "Content-Type": mimeType, + "Content-Length": Buffer.byteLength(asset) + }); + res.write(asset); + res.end(); + } catch (err) { + res.statusCode = 404; + res.write("Not Found"); + res.end(); + } + } +}; + +module.exports = cdnMock; 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 6a63e2383..6c6d727d6 100644 --- a/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js +++ b/packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js @@ -9,6 +9,7 @@ const redbird = require("@jchip/redbird"); const ck = require("chalker"); const optionalRequire = require("optional-require")(require); const { settings, searchSSLCerts, controlPaths } = require("../../config/dev-proxy"); +const cdnMock = require("./cdn-mock"); const { formUrl } = require("../utils"); @@ -115,7 +116,8 @@ const registerElectrodeDevRules = ({ port, appPort, webpackDevPort, - restart + restart, + enableCdnMock }) => { const { dev: devPath, admin: adminPath, hmr: hmrPath, appLog, reporter } = controlPaths; const appForwards = [ @@ -183,6 +185,22 @@ const registerElectrodeDevRules = ({ return false; } }); + + // mock-cdn + + if (enableCdnMock) { + const mockCdnSrc = formUrl({ protocol, host, port, path: `/__mock-cdn` }); + cdnMock.generateMockAssets(mockCdnSrc); + proxy.register({ + ssl, + src: mockCdnSrc, + target: `http://localhost:29999/__mock-cdn`, + onRequest(req, res) { + cdnMock.respondAsset(req, res); + return false; + } + }); + } }; const startProxy = inOptions => { @@ -255,8 +273,9 @@ const startProxy = inOptions => { res.end(); }); + const enableCdnMock = process.argv.includes("--mock-cdn"); // register with primary protocol/host/port - registerElectrodeDevRules({ ...options, ssl, proxy, restart }); + registerElectrodeDevRules({ ...options, ssl, proxy, restart, enableCdnMock }); // if primary protocol is https, then register regular http rules at httpPort if (ssl) { @@ -277,9 +296,11 @@ const startProxy = inOptions => { } const proxyUrl = formUrl({ protocol, host, port: options.port }); + const mockCdnMsg = enableCdnMock + ? `\nMock CDN is enabled (mapping saved to config/assets.json)\n` + : "\n"; console.log( - ck`Electrode dev proxy server running: - + ck`Electrode dev proxy server running:${mockCdnMsg} ${buildProxyTree(options, ["appPort", "webpackDevPort"])} View status at ${proxyUrl}${controlPaths.status}` ); diff --git a/packages/xarc-app-dev/package.json b/packages/xarc-app-dev/package.json index 454e2902e..d10666abd 100644 --- a/packages/xarc-app-dev/package.json +++ b/packages/xarc-app-dev/package.json @@ -55,7 +55,7 @@ "babel-plugin-transform-react-remove-prop-types": "^0.4.20", "boxen": "^4.2.0", "chalker": "^1.2.0", - "chokidar": "^2.0.4", + "chokidar": "^3.3.1", "core-js": "^3", "electrode-hapi-compat": "^1.2.0", "electrode-node-resolver": "^2.0.0", @@ -66,12 +66,13 @@ "isomorphic-loader": "^3.0.0", "lodash": "^4.13.1", "log-update": "^4.0.0", - "mime": "^1.0.0", + "mime": "^1.6.0", "mkdirp": "^0.5.1", "nix-clap": "^1.3.7", "nyc": "^14.1.1", "optional-require": "^1.0.0", "prompts": "^2.2.1", + "ps-get": "^1.0.1", "regenerator-runtime": "^0.13.2", "request": "^2.88.0", "require-at": "^1.0.2", @@ -84,8 +85,8 @@ "webpack-dev-middleware": "^3.4.0", "webpack-hot-middleware": "^2.22.2", "winston": "^2.3.1", - "xaa": "^1.4.0", - "xclap": "^0.2.48", + "xaa": "^1.5.0", + "xclap": "^0.2.50", "xenv-config": "^1.3.1", "xsh": "^0.4.4" }, @@ -119,10 +120,10 @@ }, "fyn": { "dependencies": { - "electrode-node-resolver": "../electrode-node-resolver", - "subapp-util": "../subapp-util", "@jchip/redbird": "../../../redbird", - "@xarc/webpack": "../xarc-webpack" + "@xarc/webpack": "../xarc-webpack", + "electrode-node-resolver": "../electrode-node-resolver", + "subapp-util": "../subapp-util" }, "devDependencies": { "@xarc/app": "../xarc-app", diff --git a/packages/xarc-app/arch-clap.js b/packages/xarc-app/arch-clap.js index 77d5c94ed..5dd5568e7 100644 --- a/packages/xarc-app/arch-clap.js +++ b/packages/xarc-app/arch-clap.js @@ -15,6 +15,8 @@ require.resolve(`${archetype.devArchetypeName}/package.json`); const devRequire = archetype.devRequire; const ck = devRequire("chalker"); +const xaa = devRequire("xaa"); +const { psChildren } = devRequire("ps-get"); const detectCssModule = devRequire("@xarc/webpack/lib/util/detect-css-module"); @@ -24,6 +26,10 @@ const { getWebpackStartConfig, setWebpackProfile } = devRequire( "@xarc/webpack/lib/util/custom-check" ); +const chokidar = devRequire("chokidar"); + +const { spawn } = require("child_process"); + const optFlow = devOptRequire("electrode-archetype-opt-flow"); const scanDir = devRequire("filter-scan-dir"); @@ -56,6 +62,46 @@ const logger = require("./lib/logger"); const jestTestDirectories = ["_test_", "_tests_", "__test__", "__tests__"]; +const watchExec = (files, cmd) => { + let timer; + let child; + let defer = xaa.makeDefer(); + const doExec = () => { + if (timer) { + clearTimeout(timer); + } + + timer = setTimeout(async () => { + timer = undefined; + const run = msg => { + child = true; + console.log(`${msg} '${cmd}'`); + const ch = spawn(cmd, { shell: true, stdio: "inherit" }); + ch.on("close", () => { + if (child === "restart") { + run("Restarting"); + } else { + defer.resolve(); + } + }); + child = ch; + }; + if (!child) { + run("Running"); + } else if (child.kill && child.pid) { + const ch = child; + child = "restart"; + (await psChildren(ch.pid)).reverse().forEach(c => process.kill(c.pid)); + ch.kill(); + } + }, 500); + }; + const watcher = chokidar.watch([].concat(files)); + watcher.on("change", doExec); + doExec(); + return defer.promise; +}; + // By default, the dev proxy server will be hosted from PORT (3000) // and the app from APP_SERVER_PORT (3100). // If the APP_SERVER_PORT is set to the empty string however, @@ -68,10 +114,6 @@ function quote(str) { return str.startsWith(`"`) ? str : `"${str}"`; } -function webpackConfig(file) { - return Path.join(config.webpack, file); -} - function karmaConfig(file) { return Path.join(config.karma, file); } @@ -728,6 +770,33 @@ Individual .babelrc files were generated for you in src/client and src/server debug: ["build-dev-static", "server-debug"], devbrk: ["dev --inspect-brk"], + + "mock-cloud": { + desc: `Run app locally like it's deployed to cloud with CDN mock and HTTPS proxy. + You must run clap build first and set env vars like HOST, PORT, NODE_ENV=production yourself. + options: [all options will be passed to node when starting your app server]`, + task(context) { + const mockTask = xclap.concurrent([ + "dev-proxy --mock-cdn", + xclap.serial( + () => xaa.delay(500), + () => watchExec("config/assets.json", `node ${context.args.join(" ")} lib/server`) + ) + ]); + + if (!Fs.existsSync("dist")) { + console.log("dist does not exist, running build task first."); + return xclap.serial( + "build", + () => console.log("build completed, starting mock prod mode with proxy"), + mockTask + ); + } + + return xclap.serial(() => console.log("dist exist, skipping build task"), mockTask); + } + }, + dev: { desc: `Start your app with watch in development mode with dev-admin. options: node.js --inspect can be used to debug the dev-admin`, @@ -849,12 +918,12 @@ 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 " : ""; + desc: `Start Electrode dev reverse proxy by itself - useful for running it with sudo. + options: --debug --mock-cdn`, + task(context) { + const debug = context.argOpts.debug ? "--inspect-brk " : ""; const proxySpawn = require.resolve("@xarc/app-dev/lib/dev-admin/redbird-spawn"); - return `~(tty)$node ${debug}${proxySpawn}`; + return `~(tty)$node ${debug}${proxySpawn} ${context.args.join(" ")}`; } },