Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add subapp support for preact & redux-bundler #1487

Merged
merged 1 commit into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/subapp-pbundle/.babelrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: [["@babel/env", { modules: "auto", targets: { node: "current" } }], "@babel/react"]
};
3 changes: 3 additions & 0 deletions packages/subapp-pbundle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
browser
dist
*-lock.*
64 changes: 64 additions & 0 deletions packages/subapp-pbundle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Electrode Subapp For Preact and Redux Bundler

This module mainly serve to setup subapp-web with [Preact] framework, and the support for [redux-bundler] for Electrode subapps.

It basically re-exports the module subapp-web and sets it up with [Preact] specific APIs.

- For convenience, it also exports the module `preact` as `preact`, and its `h`, `Component`, and `render` APIs.
- It adds a new `reduxBundlerLoadSubApp` API for loading subapps that use [redux-bundler].

To use, a subapp's code should be doing:

```js
/** @jsx h */
import { h, reduxBundlerLoadSubApp } from "subapp-pbundle";

import Component from "./component";

export default reduxBundlerLoadSubApp({ name: "MyComponent", Component });
```

`preact` and `preact-render-to-string` are specified as peerDependencies, so you must install them as part of your `package.json` dependencies.

## SSR App Context

This module also exports a default Preact context that SSR uses to pass in server `request` object to your React component.

ie:

```js
import { AppContext } from "subapp-pbundle";

const Component = () => {
return (
<AppContext.Consumer>
{({ isSsr, ssr, subApp }) => {
return (
<div>
IS_SSR: {`${Boolean(isSsr)}`} HAS_REQUEST: {ssr && ssr.request ? "yes" : "no"}
</div>
);
}}
</AppContext.Consumer>
);
};
```

## Support for React Router

TBD

## Support for SSR with Suspense

[Preact] Suspense support is still experimental. TBD.

## License

Copyright (c) 2016-present, WalmartLabs

Licensed under the [Apache License, Version 2.0].

[apache license, version 2.0]: https://www.apache.org/licenses/LICENSE-2.0
[preact-router]: https://www.npmjs.com/package/preact-router
[preact]: https://preactjs.com/
[redux-bundler]: https://reduxbundler.com/
140 changes: 140 additions & 0 deletions packages/subapp-pbundle/lib/framework-lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use strict";

/* eslint-disable max-statements */

const preact = require("preact");
const { default: AppContext } = require("../browser/app-context");
const prts = require("preact-render-to-string");

class FrameworkLib {
constructor(ref) {
this.ref = ref;
}

async handleSSR() {
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
if (subApp.useReactRouter && !subAppServer.StartComponent) {
throw new Error("react router is not yet supported for preact");
} else {
this.StartComponent = subAppServer.StartComponent || subApp.Component;
}

if (!this.StartComponent) {
return `<!-- serverSideRendering ${subApp.name} has no StartComponent -->`;
} else if (subApp.__redux) {
// return await this.doReduxSSR();
} else if (options.serverSideRendering === true) {
return await this.doSSR();
}

return "";
}

renderTo(element, options) {
if (options.streaming) {
throw new Error("render to stream is not yet supported for preact");
}

if (options.suspenseSsr) {
throw new Error("suspense is not yet supported for preact");
}

if (options.hydrateServerData) {
return prts.render(element);
} else {
return prts.render(element);
}
}

createTopComponent(initialProps) {
const { request } = this.ref.context.user;
const { subApp } = this.ref;

let TopComponent;
if (subApp.useReactRouter) {
const rrContext = {};
const rrProps = Object.assign(
{ location: request.url.pathname, context: rrContext },
initialProps
);
// console.log("rendering", name, "for react router", rrProps);
TopComponent = preact.createElement(this.StartComponent, rrProps);
} else {
// console.log("rendering without react router", name);
TopComponent = preact.createElement(this.StartComponent, initialProps);
}
return preact.createElement(
AppContext.Provider,
{ value: { isSsr: true, subApp, ssr: { request } } },
TopComponent
);
}

async doSSR() {
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 });
}

return await this.renderTo(this.createTopComponent(initialProps), this.ref.options);
}

// TODO for redux-bundler

// async doReduxSSR() {
// const { subApp, subAppServer, context, options } = this.ref;
// const { request } = context.user;
// // subApp.reduxReducers || subApp.reduxCreateStore) {
// // if sub app has reduxReducers or reduxCreateStore then assume it's using
// // redux data model. prepare initial state and store to render it.
// let reduxData;

// // see if app has a prepare callback, on the server side first, and then the
// // app itself, and call it. assume the object it returns would contain the
// // initial redux state data.
// const prepare = subAppServer.prepare || subApp.prepare;
// if (prepare) {
// reduxData = await prepare({ request, context });
// }

// if (!reduxData) {
// reduxData = { initialState: {} };
// }

// this.initialState = reduxData.initialState || reduxData;
// // if subapp didn't request to skip sending initial state, then stringify it
// // and attach it to the index html.
// if (subAppServer.attachInitialState !== false) {
// this.initialStateStr = JSON.stringify(this.initialState);
// }
// // next we take the initial state and create redux store from it
// this.store =
// reduxData.store ||
// (subApp.reduxCreateStore && (await subApp.reduxCreateStore(this.initialState)));
// assert(
// this.store,
// `redux subapp ${subApp.name} didn't provide store, reduxCreateStore, or reducers`
// );
// if (options.serverSideRendering === true) {
// assert(Provider, "subapp-web: react-redux Provider not available");
// // finally render the element with Redux Provider and the store created
// return await this.renderTo(
// preact.createElement(Provider, { store: this.store }, this.createTopComponent()),
// options
// );
// }
// return "";
// }
}

module.exports = FrameworkLib;
20 changes: 20 additions & 0 deletions packages/subapp-pbundle/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use strict";

const subappWeb = require("subapp-web");
const preact = require("preact");
const FrameworkLib = require("./framework-lib");
const { default: AppContext } = require("../browser/app-context");

const { h, Component, render } = preact;

subappWeb.setupFramework(FrameworkLib);

module.exports = {
...subappWeb,
AppContext,
FrameworkLib,
preact,
h,
Component,
render
};
84 changes: 84 additions & 0 deletions packages/subapp-pbundle/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"name": "subapp-pbundle",
"version": "0.0.1",
"description": "Electrode subapp support for Preact and redux-bundler",
"browser": "browser/index.js",
"main": "lib/index.js",
"scripts": {
"test": "clap check",
"coverage": "clap check",
"build": "clap -x -n compile minify",
"compile": "babel src -d browser --source-maps"
},
"keywords": [
"web",
"react",
"subapp",
"redux",
"react-router"
],
"author": "Electrode (http://www.electrode.io/)",
"contributors": [
"Joel Chen <[email protected]>"
],
"license": "Apache-2.0",
"files": [
"lib",
"browser",
"dist"
],
"dependencies": {
"subapp-util": "^1.0.2",
"subapp-web": "^1.0.0"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.3.1",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.7.7",
"babel-preset-minify": "^0.5.1",
"electrode-archetype-njs-module-dev": "^3.0.0",
"jsdom": "^15.2.1",
"preact": "^10.1.1",
"preact-render-to-string": "^5.1.3",
"run-verify": "^1.2.2"
},
"peerDependencies": {
"preact": "*",
"preact-render-to-string": "*"
},
"fyn": {
"dependencies": {
"subapp-web": "../subapp-web",
"subapp-util": "../subapp-util"
}
},
"nyc": {
"all": true,
"require": [
"@babel/register",
"mocha"
],
"reporter": [
"lcov",
"text",
"text-summary"
],
"exclude": [
"coverage",
"*clap.js",
"gulpfile.js",
"dist",
"test",
"browser",
"**/.babelrc.js"
],
"check-coverage": true,
"statements": 0,
"branches": 0,
"functions": 0,
"lines": 0,
"cache": true
}
}
7 changes: 7 additions & 0 deletions packages/subapp-pbundle/src/.babelrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
presets: [
["@babel/env", { modules: "auto" }],
"@babel/react",
process.env.MINIFY ? "minify" : undefined
].filter(x => x)
};
3 changes: 3 additions & 0 deletions packages/subapp-pbundle/src/app-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from "preact";

export default createContext({});
27 changes: 27 additions & 0 deletions packages/subapp-pbundle/src/fe-framework-lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/** @jsx h */
import { h, render, hydrate } from "preact";

class FrameworkLib {
constructor(ref) {
this.ref = ref;
}

renderStart() {
const { subApp, element, options } = this.ref;

const props = { ...options._prepared, ...options.props };
const Component = subApp.info.StartComponent || subApp.info.Component;
if (element) {
if (options.serverSideRendering) {
hydrate(<Component {...props} />, element);
} else {
render(<Component {...props} />, element);
}
} else {
// no DOM element to render into, just return subapp as a component
return <Component {...props} />;
}
}
}

export default FrameworkLib;
15 changes: 15 additions & 0 deletions packages/subapp-pbundle/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import FrameworkLib from "./fe-framework-lib";

import { setupFramework } from "subapp-web";

setupFramework(FrameworkLib);

export * from "subapp-web";

export { default as preact } from "preact";

export { h, Component, render } from "preact";

export { default as AppContext } from "./app-context";

export { FrameworkLib };
3 changes: 3 additions & 0 deletions packages/subapp-pbundle/test/mocha.opts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--require node_modules/electrode-archetype-njs-module-dev/config/test/setup.js
--require @babel/register
--recursive
Loading