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

✨ using graphql + ssr #4

Merged
merged 15 commits into from
Nov 19, 2018
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
*.log
.DS_Store
cv.pdf
.eslintcache
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/packages/api-gateway/bootstrap"
}
]
}
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# work-with-us

## Development
- `yarn`
- `yarn start:dev`

## Production
- `yarn`
- `yarn build`
- `yarn start`
42 changes: 40 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,51 @@
"license": "MIT",
"private": true,
"scripts": {
"start": "run-p start:*",
"start:api": "yarn --cwd packages/api-gateway start"
"lint": "eslint --ext js,jsx --cache packages/** --ignore-pattern node_modules --ignore-pattern build --ignore-pattern public --ignore-pattern dist",
"start": "run-p start:api",
"start:api": "yarn --cwd packages/api-gateway start",
"start:dev": "run-p start:dev:*",
"start:dev:api": "yarn --cwd packages/api-gateway start:dev",
"start:dev:ui": "BROWSER=none yarn --cwd packages/ui start",
"build": "run-p build:*",
"build:hocs": "yarn --cwd packages/ui-hoc build",
"build:ui": "yarn --cwd packages/ui build"
},
"workspaces": [
"packages/*"
],
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "9.0.0",
"eslint": "5.6.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.11.0",
"npm-run-all": "^4.1.3"
},
"eslintConfig": {
"parser": "babel-eslint",
"extends": "airbnb",
"rules": {
"react/prop-types": "off",
"object-curly-newline": [
"error",
{
"ImportDeclaration": "never"
}
],
"max-len": [
"warn",
300
],
"semi": [
"error",
"never"
]
}
}
}
4 changes: 2 additions & 2 deletions packages/api-gateway/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ require('@babel/register')({
[
'babel-plugin-styled-components',
{
pure: true
}
pure: true,
},
],
],
})
Expand Down
19 changes: 16 additions & 3 deletions packages/api-gateway/package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
{
"name": "@work-with-us/gateway",
"version": "1.0.0",
"main": "src/index.js",
"main": "./bootstrap.js",
"author": "Fabien JUIF <fabien.juif@gmail.com>",
"license": "MIT",
"private": true,
"scripts": {
"start": "node bootstrap.js"
"start": "node .",
"start:dev": "nodemon ."
},
"dependencies": {
"@babel/core": "^7.1.2",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.0.0",
"@work-with-us/data": "^1.0.0",
"@work-with-us/ui": "^1.0.0",
"apollo-cache-inmemory": "^1.0.0",
"apollo-client": "^2.4.4",
"apollo-link-schema": "^1.1.1",
"apollo-server": "^2.1.0",
"apollo-server-koa": "^2.1.0",
"babel-plugin-styled-components": "^1.8.0",
"graphql": "^14.0.2",
"graphql-tools": "^4.0.0",
"ignore-styles": "^5.0.1",
"koa": "^2.6.1",
"koa-static": "^5.0.0"
"koa-static": "^5.0.0",
"react": "^16.0.0",
"react-apollo": "^2.2.4",
"react-router-dom": "^4.3.1",
"styled-components": "^3.4.10"
},
"devDependencies": {
"nodemon": "^1.18.6"
}
}
65 changes: 65 additions & 0 deletions packages/api-gateway/src/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const crypto = require('crypto')

const toMD5 = object => crypto.createHash('md5').update(JSON.stringify(object)).digest('hex')

module.exports = ({
timeout = (/* 1 h */ 1 * 60 * 60 * 1000),
maxEntries = 100,
name = 'cache',
log = console.log,
} = {}) => {
const cache = {}

const purge = () => {
// timeout
const keysTimeout = Object
.entries(cache)
.map(([key, store]) => {
if ((store.date + timeout) > Date.now()) return undefined
return key
})
.filter(Boolean)

if (keysTimeout.length > 0) {
log(`[cache](${name}) timeout keys: [${keysTimeout}]`)
keysTimeout.forEach((key) => { delete cache[key] })
}

// max entries
const entries = Object.entries(cache)
if (entries.length <= maxEntries) return
entries.sort((a, b) => a[1].date < b[1].date)
const keysMaxEntries = entries
.filter((val, index) => index >= maxEntries)
.map(([key]) => key)
if (keysMaxEntries.length > 0) {
log(`[cache](${name}) max entries keys: [${keysMaxEntries}]`)
keysMaxEntries.forEach((key) => { delete cache[key] })
}
}

const add = (key, value) => {
purge()

cache[toMD5(key)] = {
value,
date: Date.now(),
}

return value
}

const get = (key) => {
purge()

const found = cache[toMD5(key)]
if (!found) return undefined

return found.value
}

return {
get,
add,
}
}
21 changes: 3 additions & 18 deletions packages/api-gateway/src/graphql.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
const { ApolloServer } = require('apollo-server-koa')
const typeDefs = require('./types')
const schema = require('./schema')

const proposals = [
]
const server = new ApolloServer({ schema })

const resolvers = {
Query: {
proposals: () => proposals,
},
Mutation: {
addProposal: (root, { input }) => {
proposals.push(input)
return true
}
}
}

const server = new ApolloServer({ typeDefs, resolvers })

module.exports = app => {
module.exports = (app) => {
server.applyMiddleware({ app })
return server
}
6 changes: 4 additions & 2 deletions packages/api-gateway/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ const path = require('path')
const Koa = require('koa')
const serve = require('koa-static')
const GraphQL = require('./graphql')
const react = require('./react')

const app = new Koa()
const graphql = GraphQL(app)

const staticPath = path.resolve(__dirname, '../../ui/build')
app.use(react)
app.use(async (ctx, next) => {
const react = require('./react') // eslint-disable-line global-require
return react(ctx, next)
})
app.use(serve(staticPath))

const port = 4000
Expand Down
73 changes: 56 additions & 17 deletions packages/api-gateway/src/react.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,68 @@
/* eslint-disable react/jsx-filename-extension */
import React from 'react'
import { renderToString } from 'react-dom/server'
import App from '@work-with-us/ui'
import { ServerStyleSheet } from 'styled-components'
import { ThemeProvider, ServerStyleSheet } from 'styled-components'
import { ApolloProvider, renderToStringWithData } from 'react-apollo'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { SchemaLink } from 'apollo-link-schema'
import { App, theme } from '@work-with-us/ui'
import { StaticRouter } from 'react-router-dom'
import { promisify } from 'util'
import path from 'path'
import fs from 'fs'
import schema from './schema'
import cache from './cache'

const filePath = path.resolve(__dirname, '..', '..', 'ui', 'build', 'index.html')
let htmlData

const renderCache = cache({ name: 'render-cache' })

module.exports = async (ctx, next) => {
if (ctx.path === '/') {
if (!htmlData) {
console.log('Loading html base file...')
htmlData = await promisify(fs.readFile)(filePath, 'utf8')
}

const sheet = new ServerStyleSheet()
const html = renderToString(sheet.collectStyles(<App />))
const styleTags = sheet.getStyleTags()

ctx.body = htmlData
.replace('<div id="root"></div>', `<div id="root">${html}</div>`)
.replace('<style data-src="server-css"></style>', styleTags)
} else {
const cachedItem = renderCache.get(ctx.path)
if (cachedItem) {
ctx.body = cachedItem
return
}

if (!htmlData) {
console.log('Loading html base file...')
htmlData = await promisify(fs.readFile)(filePath, 'utf8')
}

const client = new ApolloClient({
ssrMode: true,
link: new SchemaLink({ schema }),
cache: new InMemoryCache(),
})

const sheet = new ServerStyleSheet()
const reactContext = {}
const html = await renderToStringWithData(
sheet.collectStyles(
<ApolloProvider client={client}>
<ThemeProvider theme={theme}>
<StaticRouter location={ctx.path} context={reactContext}>
<App />
</StaticRouter>
</ThemeProvider>
</ApolloProvider>,
),
)

// the react application doesn't found the path
// we call the next middleware
if (reactContext.notFound) {
await next()
return
}

const styleTags = sheet.getStyleTags()
const replacedHtml = htmlData
.replace('<div id="root"></div>', `<div id="root"></div><script charset="UTF-8">window.__APOLLO_STATE = ${JSON.stringify(await client.cache.extract())}</script>`)
.replace('<div id="root"></div>', `<div id="root">${html}</div>`)
.replace('<style data-src="server-css"></style>', styleTags)

renderCache.add(ctx.path, replacedHtml)
ctx.body = replacedHtml
}
24 changes: 24 additions & 0 deletions packages/api-gateway/src/schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { makeExecutableSchema } from 'graphql-tools'
import { cv } from '@work-with-us/data'
import typeDefs from './types'

const proposals = [
]

const resolvers = {
Query: {
proposals: () => proposals,
cvs: (root, { name }) => {
if (!name) return Object.values(cv)
return [cv[name]]
},
},
Mutation: {
addProposal: (root, { input }) => {
proposals.push(input)
return true
},
},
}

module.exports = makeExecutableSchema({ typeDefs, resolvers })
10 changes: 10 additions & 0 deletions packages/api-gateway/src/types/cv/cv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const cv = `{
who: Who!
description: String
skills: [GroupSkill]!
experiences: [Experience]
}`

module.exports = `
type CV ${cv}
`
8 changes: 8 additions & 0 deletions packages/api-gateway/src/types/cv/duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const duration = `{
from: Float!
to: Float
}`

module.exports = `
type Duration ${duration}
`
8 changes: 8 additions & 0 deletions packages/api-gateway/src/types/cv/enterprise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const enterprise = `{
name: String!
color: String
}`

module.exports = `
type Enterprise ${enterprise}
`
Loading