Skip to content

Commit

Permalink
feat: support mock CDN in dev-proxy for mock prod mode (#1627)
Browse files Browse the repository at this point in the history
* feat: support mock CDN in dev-proxy for mock prod mode

* back to mime 1
  • Loading branch information
jchip authored Apr 27, 2020
1 parent 15be358 commit 638ed9b
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 20 deletions.
75 changes: 75 additions & 0 deletions packages/xarc-app-dev/lib/dev-admin/cdn-mock.js
Original file line number Diff line number Diff line change
@@ -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;
29 changes: 25 additions & 4 deletions packages/xarc-app-dev/lib/dev-admin/redbird-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -115,7 +116,8 @@ const registerElectrodeDevRules = ({
port,
appPort,
webpackDevPort,
restart
restart,
enableCdnMock
}) => {
const { dev: devPath, admin: adminPath, hmr: hmrPath, appLog, reporter } = controlPaths;
const appForwards = [
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 <green>${proxyUrl}${controlPaths.status}</>`
);
Expand Down
15 changes: 8 additions & 7 deletions packages/xarc-app-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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"
},
Expand Down Expand Up @@ -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",
Expand Down
87 changes: 78 additions & 9 deletions packages/xarc-app/arch-clap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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");
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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(" ")}`;
}
},

Expand Down

0 comments on commit 638ed9b

Please sign in to comment.