├── .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 |
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 |
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 |
25 | )
26 | }
27 | }
28 |
29 | const PostList = ({ posts }) =>
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 |
50 |
51 | )
52 | }
53 | }
54 |
55 | const ChatList = ({ chats }) =>
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 |
--------------------------------------------------------------------------------