├── .meteor ├── .gitignore ├── release ├── platforms ├── .id ├── .finished-upgraders ├── packages └── versions ├── .gitignore ├── client ├── main.js └── main.html ├── server └── main.js ├── .gitattributes ├── imports ├── server │ ├── index.js │ ├── subscriptions.js │ ├── graphql.js │ ├── routes.js │ ├── schema.js │ ├── helpers.js │ └── ssr.js ├── both │ ├── pubsub.js │ ├── api │ │ ├── users │ │ │ ├── index.js │ │ │ ├── resolvers.js │ │ │ └── users.graphql │ │ ├── chats │ │ │ ├── index.js │ │ │ ├── chats.graphql │ │ │ └── resolvers.js │ │ └── posts │ │ │ ├── index.js │ │ │ ├── posts.graphql │ │ │ └── resolvers.js │ ├── pages │ │ ├── 404.js │ │ ├── index.js │ │ └── about.js │ ├── helpers │ │ └── call-with-promise.js │ ├── routes.js │ └── components │ │ ├── accounts │ │ ├── me.js │ │ ├── login.js │ │ └── signup.js │ │ ├── posts │ │ └── index.js │ │ └── chats │ │ └── index.js └── client │ ├── accounts-config.js │ └── index.js ├── .babelrc ├── README.md ├── package.json └── .eslintrc.js /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.6.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import '/imports/client' 2 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import '/imports/server' 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /imports/server/index.js: -------------------------------------------------------------------------------- 1 | import './graphql' 2 | import './subscriptions' 3 | import './ssr' 4 | -------------------------------------------------------------------------------- /imports/both/pubsub.js: -------------------------------------------------------------------------------- 1 | import { PubSub } from 'graphql-subscriptions' 2 | export default new PubSub() 3 | -------------------------------------------------------------------------------- /imports/both/api/users/index.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | 3 | const Users = Meteor.users 4 | 5 | export default Users 6 | -------------------------------------------------------------------------------- /imports/server/subscriptions.js: -------------------------------------------------------------------------------- 1 | import { setup } from 'meteor/swydo:ddp-apollo' 2 | 3 | import schema from './schema' 4 | 5 | setup({ schema }) 6 | -------------------------------------------------------------------------------- /imports/both/api/chats/index.js: -------------------------------------------------------------------------------- 1 | import { Mongo } from 'meteor/mongo' 2 | 3 | const Chats = new Mongo.Collection('chats') 4 | 5 | export default Chats 6 | -------------------------------------------------------------------------------- /imports/both/api/posts/index.js: -------------------------------------------------------------------------------- 1 | import { Mongo } from 'meteor/mongo' 2 | 3 | const Posts = new Mongo.Collection('posts') 4 | 5 | export default Posts 6 | -------------------------------------------------------------------------------- /imports/server/graphql.js: -------------------------------------------------------------------------------- 1 | import { createApolloServer } from 'meteor/apollo' 2 | 3 | import schema from './schema' 4 | 5 | createApolloServer({ schema }) 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "babel-plugin-styled-components", 5 | { 6 | "ssr": true 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /imports/both/api/users/resolvers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Query: { 3 | me(root, params, context) { 4 | return context && context.user 5 | }, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /imports/both/api/users/users.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | _id: String 3 | emails: [Email] 4 | } 5 | 6 | type Email { 7 | address: String 8 | verified: Boolean 9 | } 10 | 11 | extend type Query { 12 | me: User 13 | } 14 | -------------------------------------------------------------------------------- /imports/both/api/posts/posts.graphql: -------------------------------------------------------------------------------- 1 | type Post { 2 | _id: String! 3 | text: String! 4 | userId: String 5 | } 6 | 7 | type Query { 8 | posts: [Post] 9 | } 10 | 11 | type Mutation { 12 | addPost(text: String!): Post 13 | } 14 | -------------------------------------------------------------------------------- /imports/both/api/chats/chats.graphql: -------------------------------------------------------------------------------- 1 | type Chat { 2 | _id: String! 3 | text: String! 4 | userId: String 5 | } 6 | 7 | extend type Query { 8 | chats: [Chat] 9 | } 10 | 11 | extend type Mutation { 12 | addChat(text: String!): Chat 13 | } 14 | 15 | type Subscription { 16 | chatAdded: Chat 17 | } 18 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | d4w56rf1cegl.pkw6dujor7ff 8 | -------------------------------------------------------------------------------- /imports/both/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import { Link } from 'react-router-dom' 4 | 5 | const NotFound = () => ( 6 |
7 | 8 | Not Found 9 | 10 |

Not Found

11 | Home 12 |
13 | ) 14 | 15 | export default NotFound 16 | -------------------------------------------------------------------------------- /imports/both/helpers/call-with-promise.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import { Promise } from 'meteor/promise' 3 | 4 | const callWithPromise = (method, params) => { 5 | const methodPromise = new Promise((resolve, reject) => { 6 | Meteor.call(method, params, (error, result) => { 7 | if (error) reject(error) 8 | resolve(result) 9 | }) 10 | }) 11 | return methodPromise 12 | } 13 | 14 | export default callWithPromise 15 | -------------------------------------------------------------------------------- /imports/both/api/posts/resolvers.js: -------------------------------------------------------------------------------- 1 | import Posts from './' 2 | 3 | export default { 4 | Query: { 5 | posts(root, params, context) { 6 | return Posts.find().fetch() 7 | }, 8 | }, 9 | 10 | Mutation: { 11 | addPost(root, params, context) { 12 | const { text } = params 13 | const { userId } = context 14 | const postId = Posts.insert({ userId, text }) 15 | return Posts.findOne({ _id: postId }) 16 | }, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /imports/client/accounts-config.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base' 2 | 3 | Accounts.onLogin(() => { 4 | const loginToken = localStorage.getItem('Meteor.loginToken') 5 | const expire = localStorage.getItem('Meteor.loginTokenExpires') 6 | document.cookie = `MeteorLoginToken=${loginToken}; expires=${new Date( 7 | expire, 8 | ).toUTCString()}; path=/` 9 | }) 10 | 11 | Accounts.onLogout(() => { 12 | document.cookie = 'MeteorLoginToken=; path=/' 13 | }) 14 | -------------------------------------------------------------------------------- /imports/both/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import { Link } from 'react-router-dom' 4 | 5 | import PostList from '../components/posts' 6 | import ChatList from '../components/chats' 7 | 8 | const Home = () => ( 9 |
10 | 11 | Home 12 | 13 |

Home

14 | About 15 |
16 |

Posts

17 | 18 |
19 |

Chats

20 | 21 |
22 | ) 23 | 24 | export default Home 25 | -------------------------------------------------------------------------------- /imports/both/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Bundle } from 'react-code-split-ssr' 3 | 4 | const NotFound = () => 5 | const Index = () => 6 | const About = () => 7 | 8 | const routes = [ 9 | { exact: true, path: '/', component: Index }, 10 | { exact: true, path: '/about', component: About }, 11 | ] 12 | 13 | const redirects = [{ from: '/test', to: '/' }] 14 | 15 | const notFoundComp = NotFound 16 | 17 | export { routes, redirects, notFoundComp } 18 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modern Meteor Boilerplate 2 | 3 | ## Try 4 | 5 | ### Install Meteor 6 | 7 | ```bash 8 | curl https://install.meteor.com/ | sh 9 | ``` 10 | 11 | ### Install npm packages 12 | 13 | ```bash 14 | meteor npm i 15 | ``` 16 | 17 | ### Start 18 | 19 | ```bash 20 | npm run start 21 | ``` 22 | 23 | ### Visit 24 | 25 | Open `http://127.0.0.1:3000` at browser. 26 | 27 | ### Related 28 | 29 | - [GraphQL Server Boilerplate](https://github.com/lzl/graphql-server-boilerplate) 30 | - [React App Boilerplate](https://github.com/lzl/react-app-boilerplate) 31 | - [REST API Boilerplate](https://github.com/lzl/rest-api-boilerplate) 32 | -------------------------------------------------------------------------------- /imports/server/routes.js: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | // import { Route, StaticRouter, Switch } from 'react-router-dom' 3 | // 4 | // import NotFound from '/imports/both/pages/404' 5 | // import Index from '/imports/both/pages/' 6 | // import About from '/imports/both/pages/about' 7 | // 8 | // export default ({ url, context }) => ( 9 | // 10 | // 11 | // 12 | // 13 | // 14 | // 15 | // 16 | // ) 17 | -------------------------------------------------------------------------------- /imports/both/pages/about.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import { Link } from 'react-router-dom' 4 | import styled from 'styled-components' 5 | 6 | import Me from '../components/accounts/me' 7 | import SignupForm from '../components/accounts/signup' 8 | import LoginForm from '../components/accounts/login' 9 | 10 | const About = () => ( 11 | 12 | 13 | About 14 | 15 |

About

16 | Home 17 |
18 | 19 | 20 | 21 |
22 | ) 23 | 24 | const Wrapper = styled.div` 25 | color: #111111; 26 | ` 27 | 28 | export default About 29 | -------------------------------------------------------------------------------- /imports/both/api/chats/resolvers.js: -------------------------------------------------------------------------------- 1 | import Chats from './' 2 | import pubsub from '/imports/both/pubsub' 3 | 4 | export default { 5 | Query: { 6 | chats(root, params, context) { 7 | return Chats.find().fetch() 8 | }, 9 | }, 10 | 11 | Mutation: { 12 | addChat(root, params, context) { 13 | const { text } = params 14 | const { userId } = context 15 | const chatId = Chats.insert({ text, userId }) 16 | const chat = Chats.findOne({ _id: chatId }) 17 | pubsub.publish('chatAdded', { chatAdded: chat }) 18 | return chat 19 | }, 20 | }, 21 | 22 | Subscription: { 23 | chatAdded: { 24 | subscribe: () => pubsub.asyncIterator('chatAdded'), 25 | }, 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /imports/server/schema.js: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from 'graphql-tools' 2 | import merge from 'lodash/merge' 3 | 4 | import UsersSchema from '/imports/both/api/users/users.graphql' 5 | import UsersResolvers from '/imports/both/api/users/resolvers' 6 | import PostsSchema from '/imports/both/api/posts/posts.graphql' 7 | import PostsResolvers from '/imports/both/api/posts/resolvers' 8 | import ChatsSchema from '/imports/both/api/chats/chats.graphql' 9 | import ChatsResolvers from '/imports/both/api/chats/resolvers' 10 | 11 | const typeDefs = [UsersSchema, PostsSchema, ChatsSchema] 12 | const resolvers = merge(UsersResolvers, PostsResolvers, ChatsResolvers) 13 | 14 | export default makeExecutableSchema({ 15 | typeDefs, 16 | resolvers, 17 | }) 18 | -------------------------------------------------------------------------------- /imports/both/components/accounts/me.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import gql from 'graphql-tag' 3 | import { graphql, withApollo } from 'react-apollo' 4 | 5 | const logout = client => Meteor.logout(() => client.resetStore()) 6 | 7 | const Me = ({ data: { loading, me }, client }) => { 8 | if (loading) return
loading...
9 | const email = me && me.emails && me.emails[0] && me.emails[0].address 10 | if (email) { 11 | return ( 12 |
13 | {email} ({me._id}) 14 |
15 | ) 16 | } else { 17 | return null 18 | } 19 | } 20 | 21 | const fetchMe = gql` 22 | query Query { 23 | me { 24 | _id 25 | emails { 26 | address 27 | } 28 | } 29 | } 30 | ` 31 | 32 | export default graphql(fetchMe)(withApollo(Me)) 33 | -------------------------------------------------------------------------------- /imports/both/components/accounts/login.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import React, { PureComponent } from 'react' 3 | import gql from 'graphql-tag' 4 | import { graphql } from 'react-apollo' 5 | 6 | class LoginForm extends PureComponent { 7 | handleSubmit = e => { 8 | e.preventDefault() 9 | const email = this.email.value 10 | const password = this.password.value 11 | Meteor.loginWithPassword(email, password, err => { 12 | if (err) return 13 | this.props.data.refetch() 14 | }) 15 | this.form.reset() 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |
(this.form = el)}> 22 | (this.email = el)} /> 23 | (this.password = el)} /> 24 | 25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | const fetchMe = gql` 32 | query Query { 33 | me { 34 | _id 35 | emails { 36 | address 37 | } 38 | } 39 | } 40 | ` 41 | 42 | export default graphql(fetchMe)(LoginForm) 43 | -------------------------------------------------------------------------------- /imports/both/components/accounts/signup.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base' 2 | import React, { PureComponent } from 'react' 3 | import gql from 'graphql-tag' 4 | import { graphql } from 'react-apollo' 5 | 6 | class SignupForm extends PureComponent { 7 | handleSubmit = e => { 8 | e.preventDefault() 9 | const email = this.email.value 10 | const password = this.password.value 11 | Accounts.createUser({ email, password }, err => { 12 | if (err) return 13 | this.props.data.refetch() 14 | }) 15 | this.form.reset() 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |
(this.form = el)}> 22 | (this.email = el)} /> 23 | (this.password = el)} /> 24 | 25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | const fetchMe = gql` 32 | query Query { 33 | me { 34 | _id 35 | emails { 36 | address 37 | } 38 | } 39 | } 40 | ` 41 | 42 | export default graphql(fetchMe)(SignupForm) 43 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base@1.3.0 # Packages every Meteor app needs to have 8 | mobile-experience@1.0.5 # Packages for a great mobile UX 9 | mongo@1.4.2 # The database Meteor supports right now 10 | static-html # Define static page content in .html files 11 | reactive-var@1.0.11 # Reactive variable for tracker 12 | tracker@1.1.3 # Meteor's client-side reactive programming library 13 | 14 | standard-minifier-css@1.4.0 # CSS minifier run for production mode 15 | standard-minifier-js@2.3.1 # JS minifier run for production mode 16 | es5-shim@4.7.0 # ECMAScript 5 compatibility for older browsers 17 | ecmascript@0.10.0 # Enable ECMAScript2015+ syntax in app code 18 | shell-server@0.3.1 # Server-side component of the `meteor shell` command 19 | 20 | # react-meteor-data 21 | apollo 22 | swydo:graphql 23 | swydo:ddp-apollo 24 | accounts-password 25 | check 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modern-meteor-boilerplate", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run", 6 | "prod": "meteor run --production", 7 | "prettier": "prettier-eslint --write", 8 | "precommit": "lint-staged" 9 | }, 10 | "lint-staged": { 11 | "*.js": [ 12 | "npm run prettier", 13 | "git add" 14 | ] 15 | }, 16 | "dependencies": { 17 | "@babel/runtime": "^7.0.0-beta.36", 18 | "apollo-cache-inmemory": "^1.1.7", 19 | "apollo-client": "^2.2.3", 20 | "apollo-link": "^1.1.0", 21 | "apollo-link-http": "^1.3.3", 22 | "apollo-link-schema": "^1.0.3", 23 | "apollo-server-express": "^1.3.2", 24 | "bcrypt": "^1.0.3", 25 | "body-parser": "^1.18.2", 26 | "express": "^4.16.2", 27 | "graphql": "^0.13.0", 28 | "graphql-subscriptions": "^0.5.7", 29 | "graphql-tag": "^2.7.3", 30 | "graphql-tools": "^2.21.0", 31 | "lodash": "^4.17.5", 32 | "lru-cache": "^4.1.1", 33 | "meteor-node-stubs": "^0.3.2", 34 | "react": "^16.2.0", 35 | "react-apollo": "^2.0.4", 36 | "react-code-split-ssr": "^0.9.2", 37 | "react-dom": "^16.2.0", 38 | "react-helmet": "^5.2.0", 39 | "react-router-dom": "^4.2.2", 40 | "styled-components": "^3.1.6" 41 | }, 42 | "devDependencies": { 43 | "babel-eslint": "^8.2.1", 44 | "babel-plugin-styled-components": "^1.5.0", 45 | "eslint": "^4.17.0", 46 | "eslint-plugin-meteor": "^4.2.0", 47 | "eslint-plugin-react": "^7.6.1", 48 | "husky": "^0.14.3", 49 | "lint-staged": "^6.1.0", 50 | "prettier": "^1.10.2", 51 | "prettier-eslint-cli": "^4.7.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /imports/client/index.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base' 2 | import React from 'react' 3 | import { hydrate } from 'react-dom' 4 | import { BrowserRouter } from 'react-router-dom' 5 | import { onPageLoad } from 'meteor/server-render' 6 | import { ApolloLink, from, split } from 'apollo-link' 7 | import { ApolloClient } from 'apollo-client' 8 | import { HttpLink } from 'apollo-link-http' 9 | import { InMemoryCache } from 'apollo-cache-inmemory' 10 | import { DDPSubscriptionLink, isSubscription } from 'meteor/swydo:ddp-apollo' 11 | import { ApolloProvider } from 'react-apollo' 12 | import { generateRoutes } from 'react-code-split-ssr' 13 | 14 | import generateRoutesProps from '/imports/both/routes' 15 | import './accounts-config' 16 | 17 | const authLink = new ApolloLink((operation, forward) => { 18 | const token = Accounts._storedLoginToken() 19 | operation.setContext(() => ({ 20 | headers: { 21 | 'meteor-login-token': token, 22 | }, 23 | })) 24 | return forward(operation) 25 | }) 26 | 27 | const httpLink = new HttpLink() 28 | 29 | const ddpLink = new DDPSubscriptionLink() 30 | 31 | const client = new ApolloClient({ 32 | link: split(isSubscription, ddpLink, from([authLink, httpLink])), 33 | cache: new InMemoryCache().restore(window.__APOLLO_STATE__), 34 | connectToDevTools: Meteor.isDevelopment, 35 | }) 36 | 37 | onPageLoad(async () => { 38 | const ClientRoutes = await generateRoutes({ 39 | ...generateRoutesProps, 40 | pathname: window.location.pathname, 41 | }) 42 | hydrate( 43 | 44 | 45 | 46 | 47 | , 48 | document.getElementById('app'), 49 | ) 50 | }) 51 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.4.2 2 | accounts-password@1.5.0 3 | allow-deny@1.1.0 4 | apollo@2.0.0 5 | autoupdate@1.4.0 6 | babel-compiler@7.0.3 7 | babel-runtime@1.2.2 8 | base64@1.0.10 9 | binary-heap@1.0.10 10 | blaze-tools@1.0.10 11 | boilerplate-generator@1.4.0 12 | caching-compiler@1.1.11 13 | caching-html-compiler@1.1.2 14 | callback-hook@1.1.0 15 | check@1.3.0 16 | ddp@1.4.0 17 | ddp-client@2.3.1 18 | ddp-common@1.4.0 19 | ddp-rate-limiter@1.0.7 20 | ddp-server@2.1.2 21 | deps@1.0.12 22 | diff-sequence@1.1.0 23 | dynamic-import@0.3.0 24 | ecmascript@0.10.3 25 | ecmascript-runtime@0.5.0 26 | ecmascript-runtime-client@0.6.1 27 | ecmascript-runtime-server@0.5.0 28 | ejson@1.1.0 29 | email@1.2.3 30 | es5-shim@4.7.3 31 | geojson-utils@1.0.10 32 | hot-code-push@1.0.4 33 | html-tools@1.0.11 34 | htmljs@1.0.11 35 | http@1.4.0 36 | id-map@1.1.0 37 | launch-screen@1.1.1 38 | livedata@1.0.18 39 | localstorage@1.2.0 40 | logging@1.1.19 41 | meteor@1.8.2 42 | meteor-base@1.3.0 43 | minifier-css@1.3.0 44 | minifier-js@2.3.1 45 | minimongo@1.4.3 46 | mobile-experience@1.0.5 47 | mobile-status-bar@1.0.14 48 | modules@0.11.4 49 | modules-runtime@0.9.2 50 | mongo@1.4.3 51 | mongo-dev-server@1.1.0 52 | mongo-id@1.0.6 53 | npm-bcrypt@0.9.3 54 | npm-mongo@2.2.34 55 | ordered-dict@1.1.0 56 | promise@0.10.1 57 | random@1.1.0 58 | rate-limit@1.0.9 59 | reactive-var@1.0.11 60 | reload@1.2.0 61 | retry@1.1.0 62 | routepolicy@1.0.12 63 | server-render@0.3.0 64 | service-configuration@1.0.11 65 | sha@1.0.9 66 | shell-server@0.3.1 67 | shim-common@0.1.0 68 | socket-stream-client@0.1.0 69 | spacebars-compiler@1.1.3 70 | srp@1.0.10 71 | standard-minifier-css@1.4.0 72 | standard-minifier-js@2.3.1 73 | static-html@1.2.2 74 | swydo:ddp-apollo@1.2.0 75 | swydo:graphql@0.4.0 76 | templating-tools@1.1.2 77 | tracker@1.1.3 78 | underscore@1.0.10 79 | url@1.2.0 80 | webapp@1.5.0 81 | webapp-hashing@1.0.9 82 | -------------------------------------------------------------------------------- /imports/server/helpers.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import { Accounts } from 'meteor/accounts-base' 3 | import { check } from 'meteor/check' 4 | 5 | // copy from 6 | // https://github.com/apollographql/meteor-integration/blob/ 7 | // 2881dc449a1d4e96b814c35ab50f22ba92cdf563/src/main-server.js#L127 8 | export const getUserForContext = async loginToken => { 9 | // there is a possible current user connected! 10 | if (loginToken) { 11 | // throw an error if the token is not a string 12 | check(loginToken, String) 13 | 14 | // the hashed token is the key to find the possible current user in the db 15 | const hashedToken = Accounts._hashLoginToken(loginToken) 16 | 17 | // get the possible current user from the database 18 | // note: no need of a fiber aware findOne + a fiber aware call break tests 19 | // runned with practicalmeteor:mocha if eslint is enabled 20 | const currentUser = await Meteor.users.rawCollection().findOne({ 21 | 'services.resume.loginTokens.hashedToken': hashedToken, 22 | }) 23 | 24 | // the current user exists 25 | if (currentUser) { 26 | // find the right login token corresponding, the current user may have 27 | // several sessions logged on different browsers / computers 28 | const tokenInformation = currentUser.services.resume.loginTokens.find( 29 | tokenInfo => tokenInfo.hashedToken === hashedToken, 30 | ) 31 | 32 | // get an exploitable token expiration date 33 | const expiresAt = Accounts._tokenExpiration(tokenInformation.when) 34 | 35 | // true if the token is expired 36 | const isExpired = expiresAt < new Date() 37 | 38 | // if the token is still valid, give access to the current user 39 | // information in the resolvers context 40 | if (!isExpired) { 41 | // return a new context object with the current user & her id 42 | return { 43 | user: currentUser, 44 | userId: currentUser._id, 45 | } 46 | } 47 | } 48 | } 49 | 50 | return {} 51 | } 52 | -------------------------------------------------------------------------------- /imports/both/components/posts/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import gql from 'graphql-tag' 3 | import { graphql, compose } from 'react-apollo' 4 | 5 | class PostContainer extends PureComponent { 6 | handleSubmit = e => { 7 | e.preventDefault() 8 | this.props.addPost({ text: this.input.value }) 9 | this.form.reset() 10 | } 11 | 12 | render() { 13 | const { data: { loading, posts } } = this.props 14 | 15 | if (loading) return
loading...
16 | 17 | return ( 18 |
19 | 20 |
(this.form = el)}> 21 | (this.input = el)} /> 22 | 23 |
24 |
25 | ) 26 | } 27 | } 28 | 29 | const PostList = ({ posts }) =>
    {posts.map(i => )}
30 | 31 | const PostItem = ({ item }) =>
  • {item.text}
  • 32 | 33 | const fetchPosts = gql` 34 | query Query { 35 | posts { 36 | _id 37 | text 38 | userId 39 | } 40 | } 41 | ` 42 | 43 | const addPost = gql` 44 | mutation addPost($text: String!) { 45 | addPost(text: $text) { 46 | _id 47 | text 48 | userId 49 | } 50 | } 51 | ` 52 | 53 | export default compose( 54 | graphql(fetchPosts), 55 | graphql(addPost, { 56 | props: ({ ownProps, mutate }) => ({ 57 | addPost: ({ text }) => 58 | mutate({ 59 | mutation: addPost, 60 | variables: { text }, 61 | optimisticResponse: { 62 | __typename: 'Mutation', 63 | addPost: { 64 | __typename: 'Post', 65 | _id: null, 66 | text, 67 | userId: null, 68 | }, 69 | }, 70 | update: (proxy, { data: { addPost } }) => { 71 | const data = proxy.readQuery({ query: fetchPosts }) 72 | data.posts.push(addPost) 73 | proxy.writeQuery({ query: fetchPosts, data }) 74 | }, 75 | }), 76 | }), 77 | }), 78 | )(PostContainer) 79 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | meteor: true, 8 | }, 9 | globals: { 10 | wx: false, 11 | }, 12 | extends: ['eslint:recommended', 'plugin:meteor/recommended', 'plugin:react/recommended'], 13 | parserOptions: { 14 | allowImportExportEverywhere: true, 15 | ecmaFeatures: { 16 | experimentalObjectRestSpread: true, 17 | jsx: true, 18 | }, 19 | sourceType: 'module', 20 | }, 21 | settings: { 22 | 'import/resolver': 'meteor', 23 | }, 24 | plugins: ['react', 'meteor'], 25 | rules: { 26 | camelcase: [2, { properties: 'never' }], 27 | curly: [2, 'multi-line'], 28 | indent: [2, 2, { SwitchCase: 1 }], 29 | quotes: [2, 'single'], 30 | semi: [2, 'never'], 31 | eqeqeq: 2, 32 | yoda: 2, 33 | 'array-callback-return': 2, 34 | 'block-spacing': 2, 35 | 'computed-property-spacing': 2, 36 | 'comma-dangle': [ 37 | 2, 38 | { 39 | arrays: 'always-multiline', 40 | objects: 'always-multiline', 41 | imports: 'always-multiline', 42 | exports: 'always-multiline', 43 | functions: 'always-multiline', 44 | }, 45 | ], 46 | 'func-call-spacing': 2, 47 | 'eol-last': 2, 48 | 'no-undefined': 2, 49 | 'no-use-before-define': 2, 50 | 'no-multi-assign': 2, 51 | 'no-useless-concat': 2, 52 | 'no-useless-return': 2, 53 | 'no-shadow-restricted-names': 2, 54 | 'no-multi-spaces': 2, 55 | 'no-multi-str': 2, 56 | 'no-unused-vars': 1, 57 | 'no-alert': 1, 58 | 'no-console': 1, 59 | 'no-useless-constructor': 1, 60 | 'no-constant-condition': [2, { checkLoops: false }], 61 | 'no-duplicate-imports': [2, { includeExports: true }], 62 | 'no-useless-computed-key': 2, 63 | 'no-useless-rename': 2, 64 | 'no-var': 2, 65 | 'space-before-blocks': 2, 66 | 'space-in-parens': 2, 67 | 'space-infix-ops': 2, 68 | 'space-unary-ops': [2, { words: true, nonwords: false }], 69 | 'space-before-function-paren': [ 70 | 2, 71 | { 72 | anonymous: 'never', 73 | named: 'never', 74 | asyncArrow: 'always', 75 | }, 76 | ], 77 | 'template-tag-spacing': 2, 78 | 'max-len': [ 79 | 2, 80 | { 81 | code: 100, 82 | tabWidth: 2, 83 | ignoreStrings: true, 84 | ignoreTrailingComments: true, 85 | ignoreTemplateLiterals: true, 86 | }, 87 | ], 88 | 'object-shorthand': 2, 89 | 'object-curly-spacing': [2, 'always'], 90 | 'prefer-const': 2, 91 | 'prefer-arrow-callback': 2, 92 | 'template-curly-spacing': [2, 'never'], 93 | 'react/prop-types': 0, 94 | 'react/no-danger': 0, 95 | 'react/display-name': 0, 96 | 'meteor/no-session': 0, 97 | 'meteor/audit-argument-checks': 0, 98 | }, 99 | } 100 | -------------------------------------------------------------------------------- /imports/server/ssr.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import React from 'react' 3 | import { StaticRouter } from 'react-router' 4 | import { onPageLoad } from 'meteor/server-render' 5 | import { Helmet } from 'react-helmet' 6 | import LRU from 'lru-cache' 7 | import { ServerStyleSheet } from 'styled-components' 8 | import { ApolloClient } from 'apollo-client' 9 | import { SchemaLink } from 'apollo-link-schema' 10 | import { InMemoryCache } from 'apollo-cache-inmemory' 11 | import { ApolloProvider, renderToStringWithData } from 'react-apollo' 12 | import { generateRoutes } from 'react-code-split-ssr' 13 | 14 | import { getUserForContext } from './helpers' 15 | import schema from './schema' 16 | import generateRoutesProps from '/imports/both/routes' 17 | 18 | const ssrCache = LRU({ 19 | max: 500, 20 | maxAge: 1000 * 30, 21 | }) 22 | 23 | const getSSRCache = async (url, context = {}) => { 24 | const { loginToken } = context 25 | const key = loginToken ? url.pathname + '.' + loginToken : url.pathname 26 | if (ssrCache.has(key)) { 27 | return ssrCache.get(key) 28 | } else { 29 | const ServerRoutes = await generateRoutes({ 30 | ...generateRoutesProps, 31 | pathname: url.pathname, 32 | }) 33 | const userContext = await getUserForContext(context.loginToken) 34 | const schemaLink = new SchemaLink({ 35 | schema, 36 | context: userContext, 37 | }) 38 | const client = new ApolloClient({ 39 | ssrMode: true, 40 | link: schemaLink, 41 | cache: new InMemoryCache(), 42 | }) 43 | const sheet = new ServerStyleSheet() 44 | const jsx = sheet.collectStyles( 45 | 46 | 47 | 48 | 49 | , 50 | ) 51 | const html = await renderToStringWithData(jsx) 52 | const state = `` 56 | const styleTags = sheet.getStyleTags() 57 | const helmet = Helmet.renderStatic() 58 | const meta = helmet.meta.toString() 59 | const title = helmet.title.toString() 60 | const link = helmet.link.toString() 61 | const newSSRCache = { loginToken, html, state, styleTags, meta, title, link } 62 | Meteor.defer(() => { 63 | const key = loginToken ? url.pathname + '.' + loginToken : url.pathname 64 | ssrCache.set(key, newSSRCache) 65 | }) 66 | return newSSRCache 67 | } 68 | } 69 | 70 | onPageLoad(async sink => { 71 | const cookies = sink.getCookies() 72 | const context = { loginToken: cookies.MeteorLoginToken } 73 | const cache = await getSSRCache(sink.request.url, context) 74 | sink.renderIntoElementById('app', cache.html) 75 | sink.appendToBody(cache.state) 76 | sink.appendToHead(cache.meta) 77 | sink.appendToHead(cache.title) 78 | sink.appendToHead(cache.link) 79 | sink.appendToHead(cache.styleTags) 80 | }) 81 | -------------------------------------------------------------------------------- /imports/both/components/chats/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import gql from 'graphql-tag' 3 | import { graphql, compose } from 'react-apollo' 4 | 5 | const chatSubscription = gql` 6 | subscription chatAdded { 7 | chatAdded { 8 | _id 9 | text 10 | userId 11 | } 12 | } 13 | ` 14 | 15 | class ChatContainer extends PureComponent { 16 | handleSubmit = e => { 17 | e.preventDefault() 18 | this.props.addChat({ text: this.input.value }) 19 | this.form.reset() 20 | } 21 | 22 | componentDidMount() { 23 | this.props.data.subscribeToMore({ 24 | document: chatSubscription, 25 | updateQuery: (prev, { subscriptionData }) => { 26 | const newChat = subscriptionData.data.chatAdded 27 | if (prev.chats.find(i => i._id === newChat._id)) { 28 | return prev 29 | } 30 | return { 31 | ...prev, 32 | chats: [...prev.chats.filter(i => i._id !== null), newChat], 33 | } 34 | }, 35 | }) 36 | } 37 | 38 | render() { 39 | const { data: { loading, chats } } = this.props 40 | 41 | if (loading) return
    loading...
    42 | 43 | return ( 44 |
    45 | 46 |
    (this.form = el)}> 47 | (this.input = el)} /> 48 | 49 |
    50 |
    51 | ) 52 | } 53 | } 54 | 55 | const ChatList = ({ chats }) =>
      {chats.map(i => )}
    56 | 57 | const ChatItem = ({ item }) =>
  • {item.text}
  • 58 | 59 | const fetchChats = gql` 60 | query Query { 61 | chats { 62 | _id 63 | text 64 | userId 65 | } 66 | } 67 | ` 68 | 69 | const addChat = gql` 70 | mutation addChat($text: String!) { 71 | addChat(text: $text) { 72 | _id 73 | text 74 | userId 75 | } 76 | } 77 | ` 78 | 79 | export default compose( 80 | graphql(fetchChats), 81 | graphql(addChat, { 82 | props: ({ ownProps, mutate }) => ({ 83 | addChat: ({ text }) => 84 | mutate({ 85 | mutation: addChat, 86 | variables: { text }, 87 | optimisticResponse: { 88 | __typename: 'Mutation', 89 | addChat: { 90 | __typename: 'Chat', 91 | _id: null, 92 | text, 93 | userId: null, 94 | }, 95 | }, 96 | update: (proxy, { data: { addChat } }) => { 97 | const data = proxy.readQuery({ query: fetchChats }) 98 | // don't double add the chat 99 | if (!data.chats.find(i => i._id === addChat._id)) { 100 | data.chats.push(addChat) 101 | } 102 | proxy.writeQuery({ query: fetchChats, data }) 103 | }, 104 | }), 105 | }), 106 | }), 107 | )(ChatContainer) 108 | --------------------------------------------------------------------------------