diff --git a/packages/subapp-react/lib/framework-lib.js b/packages/subapp-react/lib/framework-lib.js
index a94beb2e4..b3b8c3bc8 100644
--- a/packages/subapp-react/lib/framework-lib.js
+++ b/packages/subapp-react/lib/framework-lib.js
@@ -15,9 +15,11 @@ const ReactRouterDom = optionalRequire("react-router-dom");
class FrameworkLib {
constructor(ref) {
this.ref = ref;
+ this._prepared = false;
}
- async handleSSR() {
+ async handlePrepare() {
+ this._prepared = true;
const { subApp, subAppServer, options } = this.ref;
// If subapp wants to use react router and server didn't specify a StartComponent,
// then create a wrap StartComponent that uses react router's StaticRouter
@@ -27,6 +29,24 @@ class FrameworkLib {
this.StartComponent = subAppServer.StartComponent || subApp.Component;
}
+ if (this.StartComponent) {
+ if (subApp.__redux) {
+ return await this.prepareReduxData();
+ } else if (options.serverSideRendering === true) {
+ return await this.prepareData();
+ }
+ }
+
+ return false;
+ }
+
+ async handleSSR() {
+ const { subApp, options } = this.ref;
+
+ if (!this._prepared) {
+ await this.handlePrepare();
+ }
+
if (!this.StartComponent) {
return ``;
} else if (subApp.__redux) {
@@ -86,25 +106,27 @@ class FrameworkLib {
);
}
- async doSSR() {
+ async prepareData() {
const { subApp, subAppServer, context } = this.ref;
const { request } = context.user;
- let initialProps;
-
// even though we don't know what data model the component is using, but if it
// has a prepare callback, we will just call it to get initial props to pass
// to the component when rendering it
const prepare = subAppServer.prepare || subApp.prepare;
if (prepare) {
- initialProps = await prepare({ request, context });
+ this._initialProps = await prepare({ request, context });
}
- return await this.renderTo(this.createTopComponent(initialProps), this.ref.options);
+ return this._initialProps;
}
- async doReduxSSR() {
- const { subApp, subAppServer, context, options } = this.ref;
+ async doSSR() {
+ return await this.renderTo(this.createTopComponent(this._initialProps), this.ref.options);
+ }
+
+ async prepareReduxData() {
+ const { subApp, subAppServer, context } = this.ref;
const { request } = context.user;
// subApp.reduxReducers || subApp.reduxCreateStore) {
// if sub app has reduxReducers or reduxCreateStore then assume it's using
@@ -137,6 +159,11 @@ class FrameworkLib {
this.store,
`redux subapp ${subApp.name} didn't provide store, reduxCreateStore, or reducers`
);
+ }
+
+ async doReduxSSR() {
+ const { options } = this.ref;
+
if (options.serverSideRendering === true) {
assert(Provider, "subapp-web: react-redux Provider not available");
// finally render the element with Redux Provider and the store created
diff --git a/packages/subapp-react/test/spec/ssr-framework.spec.js b/packages/subapp-react/test/spec/ssr-framework.spec.js
index fa4a37176..c46ca075c 100644
--- a/packages/subapp-react/test/spec/ssr-framework.spec.js
+++ b/packages/subapp-react/test/spec/ssr-framework.spec.js
@@ -72,6 +72,26 @@ describe("SSR React framework", function() {
expect(res).contains("Hello foo bar");
});
+ it("should allow preparing data before SSR", async () => {
+ const framework = new lib.FrameworkLib({
+ subApp: {
+ prepare: () => ({ test: "foo bar" }),
+ Component: props => {
+ return
Hello {props.test}
;
+ }
+ },
+ subAppServer: {},
+ options: { serverSideRendering: true },
+ context: {
+ user: {}
+ }
+ });
+ await framework.handlePrepare();
+ expect(framework._initialProps).to.be.ok;
+ const res = await framework.handleSSR();
+ expect(res).contains("Hello foo bar");
+ });
+
it("should render Component with streaming if enabled", () => {
const framework = new lib.FrameworkLib({
subApp: {
diff --git a/packages/subapp-web/lib/load.js b/packages/subapp-web/lib/load.js
index 195749da1..caa5011b1 100644
--- a/packages/subapp-web/lib/load.js
+++ b/packages/subapp-web/lib/load.js
@@ -17,15 +17,14 @@ const Path = require("path");
const _ = require("lodash");
const retrieveUrl = require("request");
const util = require("./util");
+const xaa = require("xaa");
const { loadSubAppByName, loadSubAppServerByName } = require("subapp-util");
// global name to store client subapp runtime, ie: window.xarcV1
// V1: version 1.
const xarc = "window.xarcV1";
-module.exports = function setup(setupContext, token) {
- const options = token.props;
-
+module.exports = function setup(setupContext, { props: options }) {
// TODO: create JSON schema to validate props
// name="Header"
@@ -160,7 +159,7 @@ module.exports = function setup(setupContext, token) {
const clientProps = JSON.stringify(_.pick(options, ["useReactRouter"]));
return {
- process: context => {
+ process: (context, { props }) => {
const { request } = context.user;
if (request.app.webpackDev && subAppLoadTime < request.app.webpackDev.compileTime) {
@@ -168,17 +167,81 @@ module.exports = function setup(setupContext, token) {
loadSubApp();
}
+ let { group = "_" } = props;
+ group = [].concat(group);
+ const ssrGroups = group.map(grp =>
+ util.getOrSet(context, ["user", "xarcSubappSSR", grp], { queue: [] })
+ );
+
+ //
+ // push {awaitData, ready, renderSSR, props} into queue
+ //
+ // awaitData - promise
+ // ready - defer promise to signal SSR info is ready for processing
+ // props - token.props
+ // renderSSR - callback to start rendering SSR for the group
+ //
+
+ const ssrInfo = { props, group, ready: xaa.defer() };
+ ssrGroups.forEach(grp => grp.queue.push(ssrInfo));
+
const outputSpot = context.output.reserve();
// console.log("subapp load", name, "useReactRouter", subApp.useReactRouter);
+ const outputSSRContent = (ssrContent, initialStateStr) => {
+ // If user specified an element ID for a DOM Node to host the SSR content then
+ // add the div for the Node and the SSR content to it, and add JS to start the
+ // sub app on load.
+ let elementId = "";
+ if (options.elementId) {
+ elementId = `elementId:"${options.elementId}",\n `;
+ outputSpot.add(``);
+ outputSpot.add(ssrContent); // must add by itself since this could be a stream
+ outputSpot.add(`
`);
+ } else {
+ outputSpot.add("\n");
+ outputSpot.add(ssrContent);
+ }
+
+ outputSpot.add(`
+
+`);
+ };
+
+ const handleError = err => {
+ if (process.env.NODE_ENV !== "production") {
+ const stack = util.removeCwd(err.stack);
+ console.error(`SSR subapp ${name} failed ${stack}`); // eslint-disable-line
+ outputSpot.add(``);
+ } else if (request && request.log) {
+ request.log(["error"], { msg: `SSR subapp ${name} failed`, err });
+ }
+ };
+
+ const closeOutput = () => {
+ if (options.timestamp) {
+ outputSpot.add(``);
+ }
+
+ outputSpot.close();
+ };
+
const processSubapp = async () => {
- let ssrContent = "";
- let initialStateStr = "";
const ref = {
context,
subApp,
subAppServer,
- options
+ options,
+ ssrGroups
};
const { bundles, scripts } = await prepareSubAppSplitBundles(context);
@@ -191,29 +254,19 @@ module.exports = function setup(setupContext, token) {
if (options.serverSideRendering) {
const lib = util.getFramework(ref);
- ssrContent = await lib.handleSSR(ref);
- initialStateStr = lib.initialStateStr;
+ ssrInfo.awaitData = lib.handlePrepare();
+
+ ssrInfo.renderSSR = async () => {
+ try {
+ outputSSRContent(await lib.handleSSR(ref), lib.initialStateStr);
+ } catch (err) {
+ handleError(err);
+ } finally {
+ closeOutput();
+ }
+ };
} else {
- ssrContent = ``;
- }
-
- // If user specified an element ID for a DOM Node to host the SSR content then
- // add the div for the Node and the SSR content to it, and add JS to start the
- // sub app on load.
- if (options.elementId) {
- outputSpot.add(``);
- outputSpot.add(ssrContent); // must add by itself since this could be a stream
- outputSpot.add(`
-
-`);
- } else {
- outputSpot.add("\n");
+ outputSSRContent("");
}
};
@@ -224,23 +277,13 @@ module.exports = function setup(setupContext, token) {
try {
await processSubapp();
+ ssrInfo.ready.resolve();
} catch (err) {
- if (process.env.NODE_ENV !== "production") {
- console.error(`SSR subapp ${name} failed ${err.stack}`); // eslint-disable-line
- outputSpot.add(``);
- } else if (request && request.log) {
- request.log(["error"], { msg: `SSR subapp ${name} failed`, err });
- }
+ handleError(err);
} finally {
- if (options.timestamp) {
- outputSpot.add(``);
+ if (!ssrInfo.renderSSR) {
+ closeOutput();
}
-
- outputSpot.close();
}
};
diff --git a/packages/subapp-web/lib/start.js b/packages/subapp-web/lib/start.js
index f25e22f70..d503d5ea9 100644
--- a/packages/subapp-web/lib/start.js
+++ b/packages/subapp-web/lib/start.js
@@ -1,13 +1,49 @@
"use strict";
+const DEFAULT_CONCURRENCY = 15;
+const xaa = require("xaa");
/*
* subapp start for SSR
* Nothing needs to be done to start subapp for SSR
*/
module.exports = function setup() {
return {
- process: () => {
- return "\n\n";
+ process: (context, { props: { concurrency } }) => {
+ const { xarcSubappSSR } = context.user;
+ const startMsg = "\n\n";
+
+ if (!xarcSubappSSR) return startMsg;
+
+ concurrency = concurrency > 0 ? concurrency : DEFAULT_CONCURRENCY;
+
+ xaa.map(
+ Object.entries(xarcSubappSSR),
+ async ([, { queue }]) => {
+ await xaa.map(
+ queue,
+ async info => {
+ // make sure subapp is ready with SSR
+ if (info.ready) await info.ready.promise;
+ // and then wait for it to complete data prepare
+ // awaitData should be available once ready is awaited
+ await info.awaitData;
+ },
+ { concurrency }
+ );
+
+ // finally kick off rendering for every subapp in the group
+ await xaa.map(
+ queue,
+ async ({ renderSSR }) => {
+ if (renderSSR) await renderSSR();
+ },
+ { concurrency }
+ );
+ },
+ { concurrency }
+ );
+
+ return startMsg;
}
};
};
diff --git a/packages/subapp-web/lib/util.js b/packages/subapp-web/lib/util.js
index 5db1d2a49..3232b3a0a 100644
--- a/packages/subapp-web/lib/util.js
+++ b/packages/subapp-web/lib/util.js
@@ -12,6 +12,25 @@ let CDN_JS_BUNDLES;
let FrameworkLib;
const utils = {
+ getOrSet(obj, path, initial) {
+ const x = _.get(obj, path);
+ if (x === undefined) {
+ _.set(obj, path, initial);
+ return initial;
+ }
+ return x;
+ },
+
+ removeCwd(msg) {
+ if (!msg) return msg;
+ const cwd = process.cwd();
+ if (cwd.length > 1) {
+ const regex = new RegExp(cwd + Path.sep, "g");
+ return msg.replace(regex, `.${Path.sep}`);
+ }
+ return msg;
+ },
+
getFramework(ref) {
return new FrameworkLib(ref);
},
diff --git a/packages/subapp-web/package.json b/packages/subapp-web/package.json
index 9788495e6..11436ee22 100644
--- a/packages/subapp-web/package.json
+++ b/packages/subapp-web/package.json
@@ -31,7 +31,8 @@
"lodash": "^4.17.15",
"optional-require": "^1.0.0",
"request": "^2.88.0",
- "subapp-util": "^1.0.3"
+ "subapp-util": "^1.0.3",
+ "xaa": "^1.1.4"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
diff --git a/packages/subapp-web/test/spec/start.spec.js b/packages/subapp-web/test/spec/start.spec.js
index 28562bf1d..8bc77b729 100644
--- a/packages/subapp-web/test/spec/start.spec.js
+++ b/packages/subapp-web/test/spec/start.spec.js
@@ -4,6 +4,6 @@ const startToken = require("../../lib/start");
describe("start", function() {
it("should return subapp start HTML", () => {
- expect(startToken().process()).contains("subapp start");
+ expect(startToken().process({ user: {} }, { props: {} })).contains("subapp start");
});
});