├── .csscomb.json ├── .eslintrc ├── .github └── screenshot.png ├── .gitignore ├── README.md ├── lerna.json ├── package.json ├── packages ├── fragment-chat │ ├── .babelrc │ ├── .gitignore │ ├── app │ │ ├── Chat │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── Toggle │ │ │ └── index.js │ │ └── index.js │ ├── fragment.js │ ├── package.json │ └── webpack.config.js ├── fragment-common │ ├── .babelrc │ ├── .gitignore │ ├── common.js │ ├── fragment.js │ ├── package.json │ └── webpack.config.js ├── fragment-contacts │ ├── .babelrc │ ├── .gitignore │ ├── app │ │ ├── Contact │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── Contacts │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── PropTypes │ │ │ └── Contact │ │ │ │ └── index.js │ │ └── index.js │ ├── fragment.js │ ├── package.json │ └── webpack.config.js └── fragment-header │ ├── .babelrc │ ├── .gitignore │ ├── app │ ├── Header │ │ ├── index.js │ │ └── styles.scss │ ├── Logo │ │ ├── index.js │ │ └── styles.scss │ ├── NavItem │ │ ├── index.js │ │ └── styles.scss │ └── index.js │ ├── fragment.js │ ├── package.json │ └── webpack.config.js ├── tailor.js ├── templates └── index.html └── yarn.lock /.csscomb.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | ".git/**", 4 | "node_modules/**", 5 | "bower_components/**" 6 | ], 7 | "always-semicolon": true, 8 | "block-indent": " ", 9 | "color-case": "lower", 10 | "color-shorthand": true, 11 | "element-case": "lower", 12 | "eof-newline": true, 13 | "leading-zero": false, 14 | "quotes": "single", 15 | "remove-empty-rulesets": true, 16 | "space-after-colon": " ", 17 | "space-after-combinator": " ", 18 | "space-after-opening-brace": "\n", 19 | "space-after-selector-delimiter": " ", 20 | "space-before-closing-brace": "\n", 21 | "space-before-colon": "", 22 | "space-before-combinator": " ", 23 | "space-before-opening-brace": " ", 24 | "space-before-selector-delimiter": "", 25 | "strip-spaces": true, 26 | "unitless-zero": true, 27 | "vendor-prefix-align": true, 28 | "sort-order": [ 29 | [ 30 | "font", 31 | "font-family", 32 | "font-size", 33 | "font-weight", 34 | "font-style", 35 | "font-variant", 36 | "font-size-adjust", 37 | "font-stretch", 38 | "font-effect", 39 | "font-emphasize", 40 | "font-emphasize-position", 41 | "font-emphasize-style", 42 | "font-smooth", 43 | "src", 44 | "line-height", 45 | "position", 46 | "z-index", 47 | "top", 48 | "right", 49 | "bottom", 50 | "left", 51 | "display", 52 | "visibility", 53 | "float", 54 | "clear", 55 | "overflow", 56 | "overflow-x", 57 | "overflow-y", 58 | "-ms-overflow-x", 59 | "-ms-overflow-y", 60 | "clip", 61 | "zoom", 62 | "flex", 63 | "flex-direction", 64 | "flex-order", 65 | "flex-pack", 66 | "flex-align", 67 | "flex-wrap", 68 | "flex-grow", 69 | "flex-shrink", 70 | "align-items", 71 | "justify-content", 72 | "-webkit-box-sizing", 73 | "-moz-box-sizing", 74 | "box-sizing", 75 | "width", 76 | "min-width", 77 | "max-width", 78 | "height", 79 | "min-height", 80 | "max-height", 81 | "margin", 82 | "margin-top", 83 | "margin-right", 84 | "margin-bottom", 85 | "margin-left", 86 | "padding", 87 | "padding-top", 88 | "padding-right", 89 | "padding-bottom", 90 | "padding-left", 91 | "table-layout", 92 | "empty-cells", 93 | "caption-side", 94 | "border-spacing", 95 | "border-collapse", 96 | "list-style", 97 | "list-style-position", 98 | "list-style-type", 99 | "list-style-image", 100 | "content", 101 | "quotes", 102 | "counter-reset", 103 | "counter-increment", 104 | "resize", 105 | "cursor", 106 | "-webkit-user-select", 107 | "-moz-user-select", 108 | "-ms-user-select", 109 | "user-select", 110 | "nav-index", 111 | "nav-up", 112 | "nav-right", 113 | "nav-down", 114 | "nav-left", 115 | "-webkit-transition", 116 | "-moz-transition", 117 | "-ms-transition", 118 | "-o-transition", 119 | "transition", 120 | "-webkit-transition-delay", 121 | "-moz-transition-delay", 122 | "-ms-transition-delay", 123 | "-o-transition-delay", 124 | "transition-delay", 125 | "-webkit-transition-timing-function", 126 | "-moz-transition-timing-function", 127 | "-ms-transition-timing-function", 128 | "-o-transition-timing-function", 129 | "transition-timing-function", 130 | "-webkit-transition-duration", 131 | "-moz-transition-duration", 132 | "-ms-transition-duration", 133 | "-o-transition-duration", 134 | "transition-duration", 135 | "-webkit-transition-property", 136 | "-moz-transition-property", 137 | "-ms-transition-property", 138 | "-o-transition-property", 139 | "transition-property", 140 | "-webkit-transform", 141 | "-moz-transform", 142 | "-ms-transform", 143 | "-o-transform", 144 | "transform", 145 | "-webkit-transform-origin", 146 | "-moz-transform-origin", 147 | "-ms-transform-origin", 148 | "-o-transform-origin", 149 | "transform-origin", 150 | "-webkit-animation", 151 | "-moz-animation", 152 | "-ms-animation", 153 | "-o-animation", 154 | "animation", 155 | "-webkit-animation-name", 156 | "-moz-animation-name", 157 | "-ms-animation-name", 158 | "-o-animation-name", 159 | "animation-name", 160 | "-webkit-animation-duration", 161 | "-moz-animation-duration", 162 | "-ms-animation-duration", 163 | "-o-animation-duration", 164 | "animation-duration", 165 | "-webkit-animation-play-state", 166 | "-moz-animation-play-state", 167 | "-ms-animation-play-state", 168 | "-o-animation-play-state", 169 | "animation-play-state", 170 | "-webkit-animation-timing-function", 171 | "-moz-animation-timing-function", 172 | "-ms-animation-timing-function", 173 | "-o-animation-timing-function", 174 | "animation-timing-function", 175 | "-webkit-animation-delay", 176 | "-moz-animation-delay", 177 | "-ms-animation-delay", 178 | "-o-animation-delay", 179 | "animation-delay", 180 | "-webkit-animation-iteration-count", 181 | "-moz-animation-iteration-count", 182 | "-ms-animation-iteration-count", 183 | "-o-animation-iteration-count", 184 | "animation-iteration-count", 185 | "-webkit-animation-direction", 186 | "-moz-animation-direction", 187 | "-ms-animation-direction", 188 | "-o-animation-direction", 189 | "animation-direction", 190 | "text-align", 191 | "-webkit-text-align-last", 192 | "-moz-text-align-last", 193 | "-ms-text-align-last", 194 | "text-align-last", 195 | "vertical-align", 196 | "white-space", 197 | "text-decoration", 198 | "text-emphasis", 199 | "text-emphasis-color", 200 | "text-emphasis-style", 201 | "text-emphasis-position", 202 | "text-indent", 203 | "-ms-text-justify", 204 | "text-justify", 205 | "letter-spacing", 206 | "word-spacing", 207 | "-ms-writing-mode", 208 | "text-outline", 209 | "text-transform", 210 | "text-wrap", 211 | "text-overflow", 212 | "-ms-text-overflow", 213 | "text-overflow-ellipsis", 214 | "text-overflow-mode", 215 | "-ms-word-wrap", 216 | "word-wrap", 217 | "word-break", 218 | "-ms-word-break", 219 | "-moz-tab-size", 220 | "-o-tab-size", 221 | "tab-size", 222 | "-webkit-hyphens", 223 | "-moz-hyphens", 224 | "hyphens", 225 | "pointer-events", 226 | "opacity", 227 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", 228 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", 229 | "-ms-interpolation-mode", 230 | "color", 231 | "border", 232 | "border-width", 233 | "border-style", 234 | "border-color", 235 | "border-top", 236 | "border-top-width", 237 | "border-top-style", 238 | "border-top-color", 239 | "border-right", 240 | "border-right-width", 241 | "border-right-style", 242 | "border-right-color", 243 | "border-bottom", 244 | "border-bottom-width", 245 | "border-bottom-style", 246 | "border-bottom-color", 247 | "border-left", 248 | "border-left-width", 249 | "border-left-style", 250 | "border-left-color", 251 | "-webkit-border-radius", 252 | "-moz-border-radius", 253 | "border-radius", 254 | "-webkit-border-top-left-radius", 255 | "-moz-border-radius-topleft", 256 | "border-top-left-radius", 257 | "-webkit-border-top-right-radius", 258 | "-moz-border-radius-topright", 259 | "border-top-right-radius", 260 | "-webkit-border-bottom-right-radius", 261 | "-moz-border-radius-bottomright", 262 | "border-bottom-right-radius", 263 | "-webkit-border-bottom-left-radius", 264 | "-moz-border-radius-bottomleft", 265 | "border-bottom-left-radius", 266 | "-webkit-border-image", 267 | "-moz-border-image", 268 | "-o-border-image", 269 | "border-image", 270 | "-webkit-border-image-source", 271 | "-moz-border-image-source", 272 | "-o-border-image-source", 273 | "border-image-source", 274 | "-webkit-border-image-slice", 275 | "-moz-border-image-slice", 276 | "-o-border-image-slice", 277 | "border-image-slice", 278 | "-webkit-border-image-width", 279 | "-moz-border-image-width", 280 | "-o-border-image-width", 281 | "border-image-width", 282 | "-webkit-border-image-outset", 283 | "-moz-border-image-outset", 284 | "-o-border-image-outset", 285 | "border-image-outset", 286 | "-webkit-border-image-repeat", 287 | "-moz-border-image-repeat", 288 | "-o-border-image-repeat", 289 | "border-image-repeat", 290 | "outline", 291 | "outline-width", 292 | "outline-style", 293 | "outline-color", 294 | "outline-offset", 295 | "background", 296 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", 297 | "background-color", 298 | "background-image", 299 | "background-repeat", 300 | "background-attachment", 301 | "background-position", 302 | "background-position-x", 303 | "-ms-background-position-x", 304 | "background-position-y", 305 | "-ms-background-position-y", 306 | "-webkit-background-clip", 307 | "-moz-background-clip", 308 | "background-clip", 309 | "background-origin", 310 | "-webkit-background-size", 311 | "-moz-background-size", 312 | "-o-background-size", 313 | "background-size", 314 | "box-decoration-break", 315 | "-webkit-box-shadow", 316 | "-moz-box-shadow", 317 | "box-shadow", 318 | "filter:progid:DXImageTransform.Microsoft.gradient", 319 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", 320 | "text-shadow" 321 | ] 322 | ] 323 | } 324 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "plugins": [ 9 | "import", 10 | "react" 11 | ], 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:import/errors", 15 | "plugin:react/recommended" 16 | ], 17 | "settings": { 18 | "import/resolver": "webpack" 19 | }, 20 | "rules": { 21 | "array-bracket-spacing": [1, "always"], 22 | "comma-dangle": [1, "never"], 23 | "eqeqeq": [2, "smart"], 24 | "jsx-quotes": [1, "prefer-double"], 25 | "no-unused-vars": 0, 26 | "object-curly-spacing": [1, "always"], 27 | "quotes": [1, "single", "avoid-escape"], 28 | "react/jsx-space-before-closing": [1, "never"], 29 | "react/no-did-mount-set-state": 0, 30 | "react/prop-types": 1, 31 | "semi": [1, "never"], 32 | "space-before-blocks": [1, "always"] 33 | }, 34 | "globals": { 35 | "describe": false, 36 | "it": false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsnolan23/tailor-react-spa/f473fb1f1c3d9a4673f93c21a03cd163706d201e/.github/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailor React Single-Page Application 2 | 3 | ![Project Screenshot](./.github/screenshot.png) 4 | 5 | This repository is an example application using the [Mosaic frontend microservices architecture](https://mosaic9.org). 6 | 7 | It makes use of [Tailor](https://github.com/zalando/tailor) only, so it is a pretty basic example. 8 | 9 | Since the idea is that a separate team would be in charge of each of the fragments, there is some duplicate code within each of the fragments such as the Webpack configuration. 10 | 11 | ## How it works 12 | 13 | Tailor is a layout service. It is able to parse HTML templates and replace `` tags for their respective bundles. 14 | 15 | Tailor also injects a RequireJS bundle to your template so you're able to use Webpack Externals to share dependencies across fragments (such as `react`). 16 | 17 | ## Fragments 18 | 19 | Fragments are small applications. 20 | 21 | They might be React applications, or any other implementation. 22 | 23 | Fragments do not need to necessarily render something. 24 | 25 | This app consists basically in a couple of fragments: 26 | 27 | - fragment-common 28 | - fragment-header 29 | - fragment-contacts 30 | - fragment-chat 31 | 32 | Each fragment contains it's own `webpack.config.js` that specifies how to build it. 33 | 34 | ## Sharing dependencies with `fragment-common` 35 | 36 | Fragments have several cross-dependencies that need to be shared: 37 | 38 | - `react` 39 | - `react-dom` 40 | - `prop-types` 41 | - `classnames` 42 | - `proppy` 43 | - `proppy-react` 44 | 45 | In order to handle this, there is one fragment called `fragment-common`. 46 | 47 | This is the fragment that exports common dependencies across fragments. It is the only one of the fragments who is actually built using `umd` as a `libraryTarget`. 48 | 49 | This fragment is mostly necessary in order for you not to share `react` and other cross-fragment dependencies inside of each fragment bundle, making the bundle smaller for each fragment. 50 | 51 | ## `fragment-*` 52 | 53 | All the other fragments are parts of this application. 54 | 55 | Those shared dependencies are listed as externals in their respective webpack configurations. 56 | 57 | All of them are built using `amd` as a `libraryTarget` in their Webpack configuration files. 58 | 59 | The dependency management is handled with RequireJS on runtime. 60 | 61 | ## Setting up 62 | 63 | 1. Clone this repository using `git clone https://github.com/armand1m/mosaic-tailor-react-example.git` 64 | 1. Install all of the project dependencies with `yarn install` 65 | 1. Build the fragments with `yarn run build:fragments` 66 | 67 | ## Running 68 | 69 | 1. In one terminal, start the fragments servers with `yarn run start:fragments` 70 | 1. In another terminal, start the Tailor service with `yarn start` 71 | 1. Navigate to `http://localhost:8080` 72 | 73 | ## Running in development mode 74 | 75 | 1. In one terminal, start the fragments watchers with `yarn run watch:fragments` 76 | 1. In another terminal, start the fragments servers with `yarn run start:fragments` 77 | 1. In another terminal, start the Tailor service with `yarn start` 78 | 1. Navigate to `http://localhost:8080` 79 | 80 | ## References 81 | 82 | - [Mosaic9](https://mosaic9.org) 83 | - [Tailor Repository](https://github.com/zalando/tailor) 84 | - [The Recipe For Scalable Frontends (Zalando)](https://www.youtube.com/watch?v=m32EdvitXy4) 85 | - [Slides](https://www.slideshare.net/Codemotion/dan-persa-maximilian-fellner-the-recipe-for-scalable-frontends-codemotion-milan-2017) 86 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "version": "0.0.0" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailor-react-spa", 3 | "version": "1.0.0", 4 | "description": "An exploration into using Tailor with React", 5 | "main": "index.js", 6 | "private": true, 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "scripts": { 11 | "start": "node tailor.js", 12 | "build:fragments": "lerna run build", 13 | "start:fragments": "lerna run --parallel start", 14 | "watch:fragments": "lerna run --parallel watch" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/tsnolan23/tailor-react-spa.git" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/tsnolan23/tailor-react-spa/issues" 24 | }, 25 | "homepage": "https://github.com/tsnolan23/tailor-react-spa#readme", 26 | "devDependencies": { 27 | "babel-eslint": "^8.0.2", 28 | "eslint": "^4.11.0", 29 | "eslint-import-resolver-webpack": "^0.8.3", 30 | "eslint-plugin-import": "^2.8.0", 31 | "eslint-plugin-react": "^7.5.1", 32 | "lerna": "^3.14.1", 33 | "node-tailor": "^3.4.0", 34 | "webpack": "^3.8.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/fragment-chat/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/fragment-chat/.gitignore: -------------------------------------------------------------------------------- 1 | /public 2 | -------------------------------------------------------------------------------- /packages/fragment-chat/app/Chat/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | 5 | import Toggle from '../Toggle' 6 | 7 | import './styles.scss' 8 | 9 | const Chat = () => ( 10 | 11 | {({ on, toggle }) => ( 12 |
16 | )} 17 | 18 | ) 19 | 20 | export default Chat 21 | -------------------------------------------------------------------------------- /packages/fragment-chat/app/Chat/styles.scss: -------------------------------------------------------------------------------- 1 | .chat { 2 | position: fixed; 3 | right: 30px; 4 | bottom: 0; 5 | width: 250px; 6 | height: 30px; 7 | cursor: pointer; 8 | transition: height .3s; 9 | border-top-left-radius: 5px; 10 | border-top-right-radius: 5px; 11 | background: #5e81ac; 12 | &.expanded { 13 | height: 250px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/fragment-chat/app/Toggle/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class Toggle extends Component { 5 | constructor(props) { 6 | super(props) 7 | this.state = { on: false } 8 | this.toggle = this.toggle.bind(this) 9 | } 10 | 11 | toggle() { 12 | this.setState(prevState => ({ on: !prevState.on })) 13 | } 14 | 15 | render() { 16 | return this.props.children({ 17 | on: this.state.on, 18 | toggle: this.toggle 19 | }) 20 | } 21 | } 22 | 23 | Toggle.propTypes = { 24 | children: PropTypes.func.isRequired 25 | } 26 | 27 | export default Toggle -------------------------------------------------------------------------------- /packages/fragment-chat/app/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import Chat from './Chat' 5 | 6 | render(, document.getElementById('chat')) 7 | -------------------------------------------------------------------------------- /packages/fragment-chat/fragment.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const fs = require('fs') 4 | 5 | const server = http.createServer((req, res) => { 6 | const pathname = url.parse(req.url).pathname 7 | const jsHeader = { 'Content-Type': 'application/javascript' } 8 | switch(pathname) { 9 | case '/public/bundle.js': 10 | res.writeHead(200, jsHeader) 11 | return fs.createReadStream('./public/bundle.js').pipe(res) 12 | default: 13 | res.writeHead(200, { 14 | 'Content-Type': 'text/html', 15 | 'Link': '; rel="fragment-script"' 16 | }) 17 | return res.end('') 18 | } 19 | }) 20 | 21 | server.listen(3000, () => { 22 | console.log('SPA Fragment Server started at 3000') 23 | }) 24 | -------------------------------------------------------------------------------- /packages/fragment-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fragment-chat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "fragment.js", 6 | "scripts": { 7 | "build": "webpack -p", 8 | "watch": "webpack -w", 9 | "start": "node fragment.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "classnames": "^2.2.5", 15 | "prop-types": "^15.6.2", 16 | "react": "^16.1.1", 17 | "react-dom": "^16.1.1" 18 | }, 19 | "devDependencies": { 20 | "babel": "^6.23.0", 21 | "babel-core": "^6.26.0", 22 | "babel-loader": "^7.1.2", 23 | "babel-preset-env": "^1.6.1", 24 | "babel-preset-react": "^6.24.1", 25 | "css-loader": "^0.28.7", 26 | "node-sass": "^4.7.2", 27 | "path": "^0.12.7", 28 | "sass-loader": "^6.0.6", 29 | "style-loader": "^0.19.0", 30 | "webpack": "^3.8.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/fragment-chat/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './app/index.js', 5 | output: { 6 | path: __dirname + '/public', 7 | publicPath: 'http://localhost:8081/public/', 8 | filename: 'bundle.js', 9 | libraryTarget: 'amd' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | }, 18 | { 19 | test: /\.scss$/, 20 | loader: 'style-loader!css-loader!sass-loader' 21 | } 22 | ] 23 | }, 24 | externals: { 25 | 'react': 'react', 26 | 'react-dom': 'react-dom', 27 | 'prop-types': 'prop-types', 28 | 'classnames': 'classnames' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/fragment-common/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/fragment-common/.gitignore: -------------------------------------------------------------------------------- 1 | /public 2 | -------------------------------------------------------------------------------- /packages/fragment-common/common.js: -------------------------------------------------------------------------------- 1 | exports.react = require('react') 2 | exports['react-dom'] = require('react-dom') 3 | exports['prop-types'] = require('prop-types') 4 | exports.classnames = require('classnames') 5 | exports.proppy = require('proppy') 6 | exports['proppy-react'] = require('proppy-react') -------------------------------------------------------------------------------- /packages/fragment-common/fragment.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const fs = require('fs') 4 | 5 | const server = http.createServer((req, res) => { 6 | const pathname = url.parse(req.url).pathname 7 | const jsHeader = { 'Content-Type': 'application/javascript' } 8 | switch(pathname) { 9 | case '/public/bundle.js': 10 | res.writeHead(200, jsHeader) 11 | return fs.createReadStream('./public/bundle.js').pipe(res) 12 | default: 13 | res.writeHead(200, { 14 | 'Content-Type': 'text/html', 15 | 'Link': '; rel="fragment-script"' 16 | }) 17 | return res.end('') 18 | } 19 | }) 20 | 21 | server.listen(6006, () => { 22 | console.log('SPA Fragment Server started at 6006') 23 | }) 24 | -------------------------------------------------------------------------------- /packages/fragment-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fragment-common", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "fragment.js", 6 | "scripts": { 7 | "build": "webpack -p", 8 | "watch": "webpack -w", 9 | "start": "node fragment.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "babel": "^6.23.0", 15 | "babel-core": "^6.26.0", 16 | "babel-loader": "^7.1.2", 17 | "babel-preset-env": "^1.6.1", 18 | "babel-preset-react": "^6.24.1", 19 | "webpack": "^3.8.1" 20 | }, 21 | "dependencies": { 22 | "classnames": "^2.2.5", 23 | "prop-types": "^15.6.2", 24 | "proppy": "^1.2.2", 25 | "proppy-react": "^1.2.2", 26 | "react": "^16.1.1", 27 | "react-dom": "^16.1.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/fragment-common/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './common.js', 5 | output: { 6 | path: __dirname + '/public', 7 | publicPath: 'http://localhost:8081/public/', 8 | filename: 'bundle.js', 9 | libraryTarget: 'umd' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/fragment-contacts/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/fragment-contacts/.gitignore: -------------------------------------------------------------------------------- 1 | /public 2 | -------------------------------------------------------------------------------- /packages/fragment-contacts/app/Contact/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import ContactPropType from '../PropTypes/Contact' 5 | 6 | import './styles.scss' 7 | 8 | const Contact = ({ 9 | contact 10 | }) => ( 11 |
12 |
13 | 14 |
15 | {contact.name.first} 16 | {contact.name.last} 17 |
18 | {contact.email} 19 |
20 |
21 | ) 22 | 23 | Contact.propTypes = { 24 | contact: ContactPropType.isRequired 25 | } 26 | 27 | export default Contact 28 | -------------------------------------------------------------------------------- /packages/fragment-contacts/app/Contact/styles.scss: -------------------------------------------------------------------------------- 1 | .contact { 2 | display: flex; 3 | flex: 1; 4 | box-sizing: border-box; 5 | padding: 10px; 6 | cursor: pointer; 7 | } 8 | 9 | .contact-details { 10 | flex: 1; 11 | border-radius: 3px; 12 | background: #d8dee9; 13 | padding: 20px; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | } 19 | 20 | .contact-avatar { 21 | border-radius: 50%; 22 | border: 2px solid #eee; 23 | width: 80px; 24 | } 25 | 26 | .contact-name { 27 | margin-top: 10px; 28 | } 29 | 30 | .contact-first-name, 31 | .contact-last-name { 32 | text-transform: capitalize; 33 | } 34 | 35 | .contact-first-name:after { 36 | content: ' '; 37 | } 38 | 39 | .contact-email { 40 | display: none; 41 | } 42 | 43 | @media (min-width: 64em) { 44 | .contact-email { 45 | display: initial; 46 | } 47 | } -------------------------------------------------------------------------------- /packages/fragment-contacts/app/Contacts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { compose, withState, didSubscribe } from 'proppy' 4 | import { attach } from 'proppy-react' 5 | 6 | import Contact from '../Contact' 7 | import ContactPropType from '../PropTypes/Contact' 8 | 9 | import './styles.scss' 10 | 11 | const withContacts = compose( 12 | withState('contacts', 'setContacts', []), 13 | didSubscribe(props => { 14 | fetch('https://randomuser.me/api/?results=20') 15 | .then(response => response.json()) 16 | .then(data => data.results) 17 | .then(props.setContacts) 18 | }) 19 | ); 20 | 21 | const Contacts = ({ 22 | contacts 23 | }) => ( 24 |
25 | {contacts.map((contact, index) => ( 26 | 30 | ))} 31 |
32 | ) 33 | 34 | Contacts.propTypes = { 35 | contacts: PropTypes.arrayOf(ContactPropType) 36 | } 37 | 38 | Contacts.defaultProps = { 39 | contacts: [] 40 | } 41 | 42 | export default attach(withContacts)(Contacts) 43 | -------------------------------------------------------------------------------- /packages/fragment-contacts/app/Contacts/styles.scss: -------------------------------------------------------------------------------- 1 | .contacts { 2 | display: grid; 3 | box-sizing: border-box; 4 | margin-top: 80px; 5 | padding: 20px; 6 | } 7 | 8 | @media (min-width: 40em) { 9 | .contacts { 10 | grid-template-columns: 50% 50%; 11 | } 12 | } 13 | 14 | @media (min-width: 52em) { 15 | .contacts { 16 | grid-template-columns: 33% 33% 33%; 17 | } 18 | } 19 | 20 | @media (min-width: 64em) { 21 | .contacts { 22 | grid-template-columns: 25% 25% 25% 25%; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/fragment-contacts/app/PropTypes/Contact/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | shape, 3 | string 4 | } from 'prop-types' 5 | 6 | export default shape({ 7 | id: shape({ 8 | name: string, 9 | value: string 10 | }), 11 | email: string, 12 | name: shape({ 13 | title: string, 14 | first: string, 15 | last: string 16 | }), 17 | picture: shape({ 18 | large: string, 19 | medium: string, 20 | small: string 21 | }) 22 | }) -------------------------------------------------------------------------------- /packages/fragment-contacts/app/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import Contacts from './Contacts' 5 | 6 | render(, document.getElementById('contacts')) 7 | -------------------------------------------------------------------------------- /packages/fragment-contacts/fragment.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const fs = require('fs') 4 | 5 | const server = http.createServer((req, res) => { 6 | const pathname = url.parse(req.url).pathname 7 | const jsHeader = { 'Content-Type': 'application/javascript' } 8 | switch(pathname) { 9 | case '/public/bundle.js': 10 | res.writeHead(200, jsHeader) 11 | return fs.createReadStream('./public/bundle.js').pipe(res) 12 | default: 13 | res.writeHead(200, { 14 | 'Content-Type': 'text/html', 15 | 'Link': '; rel="fragment-script"' 16 | }) 17 | return res.end('') 18 | } 19 | }) 20 | 21 | server.listen(5000, () => { 22 | console.log('SPA Fragment Server started at 5000') 23 | }) 24 | -------------------------------------------------------------------------------- /packages/fragment-contacts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fragment-contacts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "fragment.js", 6 | "scripts": { 7 | "build": "webpack -p", 8 | "watch": "webpack -w", 9 | "start": "node fragment.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "classnames": "^2.2.5", 15 | "prop-types": "^15.6.2", 16 | "proppy": "^1.2.2", 17 | "proppy-react": "^1.2.2", 18 | "react": "^16.1.1", 19 | "react-dom": "^16.1.1" 20 | }, 21 | "devDependencies": { 22 | "babel": "^6.23.0", 23 | "babel-core": "^6.26.0", 24 | "babel-loader": "^7.1.2", 25 | "babel-preset-env": "^1.6.1", 26 | "babel-preset-react": "^6.24.1", 27 | "css-loader": "^0.28.7", 28 | "node-sass": "^4.7.2", 29 | "sass-loader": "^6.0.6", 30 | "style-loader": "^0.19.0", 31 | "webpack": "^3.8.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/fragment-contacts/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './app/index.js', 5 | output: { 6 | path: __dirname + '/public', 7 | publicPath: 'http://localhost:8081/public/', 8 | filename: 'bundle.js', 9 | libraryTarget: 'amd' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | }, 18 | { 19 | test: /\.scss$/, 20 | loader: 'style-loader!css-loader!sass-loader' 21 | } 22 | ] 23 | }, 24 | externals: { 25 | 'react': 'react', 26 | 'react-dom': 'react-dom', 27 | 'prop-types': 'prop-types', 28 | 'proppy': 'proppy', 29 | 'proppy-react': 'proppy-react', 30 | 'classnames': 'classnames' 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/fragment-header/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/fragment-header/.gitignore: -------------------------------------------------------------------------------- 1 | /public 2 | -------------------------------------------------------------------------------- /packages/fragment-header/app/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { compose, withProps, withState } from 'proppy' 4 | import { attach } from 'proppy-react' 5 | 6 | import Logo from '../Logo' 7 | import NavItem from '../NavItem' 8 | 9 | import './styles.scss' 10 | 11 | const withHeaderState = compose( 12 | withState('active', 'setActive', 1), 13 | withProps({ items: [ 0, 1, 2 ,3 ] }) 14 | ) 15 | 16 | const Header = ({ 17 | items, 18 | active, 19 | setActive 20 | }) => ( 21 |
22 | 23 | { 24 | items.map((item, index) => ( 25 | 31 | )) 32 | } 33 |
34 | ) 35 | 36 | Header.propTypes = { 37 | items: PropTypes.arrayOf(PropTypes.number).isRequired, 38 | active: PropTypes.number.isRequired, 39 | setActive: PropTypes.func.isRequired 40 | } 41 | 42 | export default attach(withHeaderState)(Header) 43 | -------------------------------------------------------------------------------- /packages/fragment-header/app/Header/styles.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | z-index: 10; 4 | top: 0; 5 | left: 0; 6 | display: flex; 7 | align-items: center; 8 | box-sizing: border-box; 9 | width: 100%; 10 | height: 80px; 11 | padding: 0 30px; 12 | background: #5e81ac; 13 | } 14 | -------------------------------------------------------------------------------- /packages/fragment-header/app/Logo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './styles.scss' 3 | 4 | const Logo = () =>
5 | 6 | export default Logo 7 | -------------------------------------------------------------------------------- /packages/fragment-header/app/Logo/styles.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | width: 100px; 3 | height: 40px; 4 | margin-right: 100px; 5 | border-radius: 2px; 6 | background: #8fbcbb; 7 | } 8 | -------------------------------------------------------------------------------- /packages/fragment-header/app/NavItem/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | import './styles.scss' 6 | 7 | const NavItem = ({ 8 | index, 9 | active, 10 | onClick 11 | }) => ( 12 |
onClick(index)} 14 | className={classnames({ 15 | 'nav-item': true, 16 | 'current': active 17 | })} 18 | > 19 |
20 | ) 21 | 22 | NavItem.propTypes = { 23 | active: PropTypes.bool, 24 | index: PropTypes.number.isRequired, 25 | onClick: PropTypes.func 26 | } 27 | 28 | NavItem.defaultProps = { 29 | active: false, 30 | onClick: () => {} 31 | } 32 | 33 | export default NavItem 34 | -------------------------------------------------------------------------------- /packages/fragment-header/app/NavItem/styles.scss: -------------------------------------------------------------------------------- 1 | .nav-item { 2 | width: 100px; 3 | height: 18px; 4 | margin-right: 20px; 5 | cursor: pointer; 6 | transition: all .3s; 7 | border-radius: 3px; 8 | background: #81a1c1; 9 | &:not(.current):hover { 10 | background: lighten(#81a1c1, 10%); 11 | } 12 | &.current { 13 | background: #ebcb8b; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/fragment-header/app/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import Header from './Header' 5 | 6 | render(
, document.getElementById('header')) 7 | -------------------------------------------------------------------------------- /packages/fragment-header/fragment.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const fs = require('fs') 4 | 5 | const server = http.createServer((req, res) => { 6 | const pathname = url.parse(req.url).pathname 7 | const jsHeader = { 'Content-Type': 'application/javascript' } 8 | switch(pathname) { 9 | case '/public/bundle.js': 10 | res.writeHead(200, jsHeader) 11 | return fs.createReadStream('./public/bundle.js').pipe(res) 12 | default: 13 | res.writeHead(200, { 14 | 'Content-Type': 'text/html', 15 | 'Link': '; rel="fragment-script"' 16 | }) 17 | return res.end('') 18 | } 19 | }) 20 | 21 | server.listen(8081, () => { 22 | console.log('SPA Fragment Server started at 8081') 23 | }) 24 | -------------------------------------------------------------------------------- /packages/fragment-header/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fragment-header", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "fragment.js", 6 | "dependencies": { 7 | "classnames": "^2.2.5", 8 | "prop-types": "^15.6.0", 9 | "proppy": "^1.2.2", 10 | "proppy-react": "^1.2.2", 11 | "react": "^16.1.1", 12 | "react-dom": "^16.1.1" 13 | }, 14 | "scripts": { 15 | "build": "webpack -p", 16 | "watch": "webpack -w", 17 | "start": "node fragment.js" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "babel": "^6.23.0", 23 | "babel-core": "^6.26.0", 24 | "babel-loader": "^7.1.2", 25 | "babel-preset-env": "^1.6.1", 26 | "babel-preset-react": "^6.24.1", 27 | "css-loader": "^0.28.7", 28 | "node-sass": "^4.7.2", 29 | "sass-loader": "^6.0.6", 30 | "style-loader": "^0.19.0", 31 | "webpack": "^3.8.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/fragment-header/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './app/index.js', 5 | output: { 6 | path: __dirname + '/public', 7 | publicPath: 'http://localhost:8081/public/', 8 | filename: 'bundle.js', 9 | libraryTarget: 'amd' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | }, 18 | { 19 | test: /\.scss$/, 20 | loader: 'style-loader!css-loader!sass-loader' 21 | } 22 | ] 23 | }, 24 | externals: { 25 | 'react': 'react', 26 | 'react-dom': 'react-dom', 27 | 'prop-types': 'prop-types', 28 | 'classnames': 'classnames', 29 | 'proppy': 'proppy', 30 | 'proppy-react': 'proppy-react' 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tailor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const Tailor = require('node-tailor') 5 | const tailor = new Tailor({ 6 | templatesPath: __dirname + '/templates' 7 | }) 8 | 9 | http 10 | .createServer((req, res) => { 11 | if (req.url === '/favicon.ico') { 12 | res.writeHead(200, { 'Content-Type': 'image/x-icon' }) 13 | return res.end('') 14 | } 15 | 16 | req.headers['x-request-uri'] = req.url 17 | req.url = '/index' 18 | 19 | tailor.requestHandler(req, res) 20 | }) 21 | .listen(8080, function() { 22 | console.log('Tailor server listening on port 8080') 23 | }) 24 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tailor Example Application 5 | 6 | 11 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | --------------------------------------------------------------------------------