commit
38c949139b
27 changed files with 7309 additions and 0 deletions
@ -0,0 +1,6 @@ |
|||||||
|
.env |
||||||
|
dist |
||||||
|
node_modules |
||||||
|
yarn-error.log |
||||||
|
.*.swp |
||||||
|
|
||||||
@ -0,0 +1,80 @@ |
|||||||
|
{ |
||||||
|
"name": "haunted-apollo", |
||||||
|
"version": "1.0.0", |
||||||
|
"private": true, |
||||||
|
"license": "MIT", |
||||||
|
"scripts": { |
||||||
|
"build": "run-s build:server build:web", |
||||||
|
"build:server": "webpack --config webpack.server.production.js", |
||||||
|
"build:web": "webpack --config webpack.web.production.js", |
||||||
|
"build:server:once": "webpack --config webpack.server.development.js", |
||||||
|
"dev:server": "yarn build:server:once && run-p nodemon:prod watch:server", |
||||||
|
"watch:server": "webpack --config webpack.server.development.js --watch", |
||||||
|
"nodemon:prod": "nodemon dist/server/server.js" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@apollo/react-hooks": "^3.1.3", |
||||||
|
"@webcomponents/webcomponentsjs": "^2.4.2", |
||||||
|
"apollo-boost": "^0.4.7", |
||||||
|
"apollo-server-express": "^2.10.1", |
||||||
|
"dotenv": "^8.2.0", |
||||||
|
"express": "^4.17.1", |
||||||
|
"graphql-tag": "^2.10.3", |
||||||
|
"haunted": "^4.7.0", |
||||||
|
"lit-html": "^1.1.2", |
||||||
|
"react-media-hook": "^0.4.4", |
||||||
|
"uuid": "^7.0.1", |
||||||
|
"yup": "^0.28.1" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@types/express": "^4.17.2", |
||||||
|
"@types/webpack-dev-middleware": "^3.7.0", |
||||||
|
"@types/webpack-hot-middleware": "^2.25.0", |
||||||
|
"@types/yup": "^0.26.32", |
||||||
|
"@typescript-eslint/eslint-plugin": "^2.20.0", |
||||||
|
"@typescript-eslint/parser": "^2.20.0", |
||||||
|
"clean-webpack-plugin": "^3.0.0", |
||||||
|
"copy-webpack-plugin": "^5.1.1", |
||||||
|
"css-loader": "^3.4.2", |
||||||
|
"dotenv-webpack": "^1.7.0", |
||||||
|
"eslint": "^6.8.0", |
||||||
|
"extract-loader": "^4.0.3", |
||||||
|
"file-loader": "^5.1.0", |
||||||
|
"html-webpack-plugin": "^3.2.0", |
||||||
|
"node-sass": "^4.13.1", |
||||||
|
"nodemon": "^2.0.2", |
||||||
|
"npm-run-all": "^4.1.5", |
||||||
|
"raw-loader": "^4.0.0", |
||||||
|
"sass-loader": "^8.0.2", |
||||||
|
"ts-loader": "^6.2.1", |
||||||
|
"typescript": "^3.7.5", |
||||||
|
"webpack": "^4.41.6", |
||||||
|
"webpack-cli": "^3.3.11", |
||||||
|
"webpack-dev-middleware": "^3.7.2", |
||||||
|
"webpack-hot-middleware": "^2.25.0", |
||||||
|
"webpack-merge": "^4.2.2", |
||||||
|
"webpack-node-externals": "^1.7.2" |
||||||
|
}, |
||||||
|
"nodemonConfig": { |
||||||
|
"exec": "node -r dotenv/config", |
||||||
|
"watch": [ |
||||||
|
"dist/server" |
||||||
|
] |
||||||
|
}, |
||||||
|
"eslintConfig": { |
||||||
|
"parser": "@typescript-eslint/parser", |
||||||
|
"extends": [ |
||||||
|
"eslint:recommended", |
||||||
|
"plugin:@typescript-eslint/eslint-recommended", |
||||||
|
"plugin:@typescript-eslint/recommended" |
||||||
|
], |
||||||
|
"parserOptions": { |
||||||
|
"ecmaVersion": 2018, |
||||||
|
"sourceType": "module" |
||||||
|
}, |
||||||
|
"rules": { |
||||||
|
"@typescript-eslint/no-explicit-any": 0, |
||||||
|
"@typescript-eslint/explicit-function-return-type": 0 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title> |
||||||
|
<script src="webcomponents-loader.js"></script> |
||||||
|
</head> |
||||||
|
<body style="margin: 0;"> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
import { gql, ApolloServer } from 'apollo-server-express'; |
||||||
|
import { v4 as uuid } from 'uuid'; |
||||||
|
|
||||||
|
const typeDefs = gql` |
||||||
|
type Book { |
||||||
|
id: String! |
||||||
|
title: String! |
||||||
|
author: String! |
||||||
|
} |
||||||
|
|
||||||
|
type Query { |
||||||
|
books: [Book!] |
||||||
|
} |
||||||
|
|
||||||
|
type Mutation { |
||||||
|
addBook(title: String!, author: String!): Book |
||||||
|
delBook(id: String!): Book |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const BOOKS = [ |
||||||
|
{ id: uuid(), title: "Hyperion", author: "Dan Simmons" }, |
||||||
|
{ id: uuid(), title: "Dune", author: "Frank Herbert" }, |
||||||
|
]; |
||||||
|
|
||||||
|
const resolvers = { |
||||||
|
Query: { |
||||||
|
books: () => { |
||||||
|
return BOOKS; |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
Mutation: { |
||||||
|
addBook: (_, { title, author }) => { |
||||||
|
const entry = { id: uuid(), title, author }; |
||||||
|
BOOKS.push(entry); |
||||||
|
return entry; |
||||||
|
}, |
||||||
|
delBook: (_, { id }) => { |
||||||
|
const idx = BOOKS.findIndex(book => book.id === id); |
||||||
|
|
||||||
|
if (idx === -1) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
const book = BOOKS[idx]; |
||||||
|
BOOKS.splice(idx, 1); |
||||||
|
return book; |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const server = new ApolloServer({ |
||||||
|
typeDefs, |
||||||
|
resolvers, |
||||||
|
}); |
||||||
|
|
||||||
|
export default server; |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
import express from 'express'; |
||||||
|
import server from './apollo'; |
||||||
|
import webpack from 'webpack'; |
||||||
|
import webpackDevMiddleware from 'webpack-dev-middleware'; |
||||||
|
import webpackHotMiddleware from 'webpack-hot-middleware'; |
||||||
|
import webpackConfig from '../../webpack.web.development.js'; |
||||||
|
|
||||||
|
const app = express(); |
||||||
|
|
||||||
|
const compiler = webpack(webpackConfig); |
||||||
|
|
||||||
|
app.use(webpackDevMiddleware(compiler)); |
||||||
|
app.use(webpackHotMiddleware(compiler)); |
||||||
|
|
||||||
|
server.applyMiddleware({ app }); |
||||||
|
|
||||||
|
app.listen(8080, () => { |
||||||
|
console.log('Server started'); |
||||||
|
}); |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
import express from 'express'; |
||||||
|
import server from './apollo'; |
||||||
|
|
||||||
|
const app = express(); |
||||||
|
|
||||||
|
server.applyMiddleware({ app }); |
||||||
|
|
||||||
|
app.use(express.static('dist/web')); |
||||||
|
|
||||||
|
app.listen(8080, () => { |
||||||
|
console.log('Server started'); |
||||||
|
}); |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
:host { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
|
||||||
|
input, button { |
||||||
|
font-size: 24px; |
||||||
|
padding: 5px 10px; |
||||||
|
border: solid 1px #ddd; |
||||||
|
&:focus { |
||||||
|
outline: none; |
||||||
|
border: solid 1px #aaa; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
background-color: #dfc; |
||||||
|
color: #555; |
||||||
|
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2); |
||||||
|
|
||||||
|
&:active { |
||||||
|
background-color: #ceb; |
||||||
|
} |
||||||
|
|
||||||
|
&:disabled { |
||||||
|
background-color: #ddd; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
import { component, html, useState } from 'haunted'; |
||||||
|
import { useMutation } from '@apollo/react-hooks'; |
||||||
|
import * as yup from 'yup'; |
||||||
|
import { AddBook, GetBooks } from './queries' |
||||||
|
import Css from './bookform.scss'; |
||||||
|
|
||||||
|
const schema = yup.object().shape({ |
||||||
|
title: yup.string().required(), |
||||||
|
author: yup.string().required(), |
||||||
|
}); |
||||||
|
|
||||||
|
const BookForm = () => { |
||||||
|
const [title, setTitle] = useState(""); |
||||||
|
const [author, setAuthor] = useState(""); |
||||||
|
const [addBook, { loading }] = useMutation(AddBook); |
||||||
|
|
||||||
|
return html` |
||||||
|
<style>${Css.toString()}</style> |
||||||
|
<input type="text" |
||||||
|
placeholder="book title" |
||||||
|
.value=${title} |
||||||
|
@change=${(e: any) => setTitle(e.target.value)}> |
||||||
|
<input type="text" |
||||||
|
placeholder="author name" |
||||||
|
.value=${author} |
||||||
|
@change=${(e: any) => setAuthor(e.target.value)}> |
||||||
|
<button @click=${async () => { |
||||||
|
try { |
||||||
|
const data = await schema.validate({ author, title }); |
||||||
|
await addBook({ variables: data, update: (cache, result) => { |
||||||
|
const list: any = cache.readQuery({ query: GetBooks }); |
||||||
|
cache.writeQuery({ query: GetBooks, data: { books: [...list.books, result.data.addBook] }}); |
||||||
|
}}); |
||||||
|
setTitle(""); |
||||||
|
setAuthor(""); |
||||||
|
} catch(e) { |
||||||
|
console.log(e); |
||||||
|
if (e instanceof yup.ValidationError) { |
||||||
|
console.log("validation error"); |
||||||
|
} |
||||||
|
} |
||||||
|
}} ?disabled=${loading}>Add book</button> |
||||||
|
`;
|
||||||
|
} |
||||||
|
|
||||||
|
customElements.define('book-form', component(BookForm)); |
||||||
|
|
||||||
@ -0,0 +1,22 @@ |
|||||||
|
:host { |
||||||
|
display: block; |
||||||
|
font-size: 24px; |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
border-bottom: solid 1px #cdf; |
||||||
|
|
||||||
|
.author { |
||||||
|
font-size: 20px; |
||||||
|
font-style: italic; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: #cdf; |
||||||
|
font-size: 14px; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,32 @@ |
|||||||
|
import { component, html } from 'haunted'; |
||||||
|
import { useQuery, useMutation } from '@apollo/react-hooks'; |
||||||
|
import { GetBooks, DelBook } from './queries'; |
||||||
|
import Css from './booklist.scss'; |
||||||
|
|
||||||
|
const BookList = () => { |
||||||
|
const { data, loading } = useQuery(GetBooks); |
||||||
|
const [delBook] = useMutation(DelBook); |
||||||
|
|
||||||
|
if (loading) { |
||||||
|
return html`<h1>Loading...</h1>`; |
||||||
|
} |
||||||
|
|
||||||
|
return html` |
||||||
|
<style>${Css.toString()}</style> |
||||||
|
${data.books.map((book: any) => html` |
||||||
|
<p>${book.title} <span class="author">by ${book.author}</span> |
||||||
|
<a href="#" @click=${async (e: any) => { |
||||||
|
e.preventDefault(); |
||||||
|
await delBook({ variables: { id: book.id }, update: (cache) => { |
||||||
|
const list: any = cache.readQuery({ query: GetBooks }); |
||||||
|
const cleaned = list.books.filter((b: any) => b.id !== book.id); |
||||||
|
cache.writeQuery({ query: GetBooks, data: { books: cleaned } }); |
||||||
|
} }); |
||||||
|
}}>[delete]</a> |
||||||
|
</p>` |
||||||
|
)} |
||||||
|
`;
|
||||||
|
} |
||||||
|
|
||||||
|
customElements.define('book-list', component(BookList)); |
||||||
|
|
||||||
@ -0,0 +1,18 @@ |
|||||||
|
import { html, render } from 'haunted'; |
||||||
|
import ApolloClient from 'apollo-boost'; |
||||||
|
import { getApolloContext } from '@apollo/react-hooks'; |
||||||
|
import './myapp'; |
||||||
|
|
||||||
|
const client = new ApolloClient({ |
||||||
|
uri: '/graphql', |
||||||
|
}); |
||||||
|
|
||||||
|
const ApolloContext = getApolloContext(); |
||||||
|
customElements.define('apollo-provider', ApolloContext.Provider); |
||||||
|
|
||||||
|
render(html` |
||||||
|
<apollo-provider .value=${{client}}> |
||||||
|
<my-app></my-app> |
||||||
|
</apollo-provider> |
||||||
|
`, document.body);
|
||||||
|
|
||||||
@ -0,0 +1,17 @@ |
|||||||
|
:host { |
||||||
|
font-family: Ubuntu, sans-serif; |
||||||
|
display: block; |
||||||
|
& > * { |
||||||
|
padding: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
.version { |
||||||
|
font-size: 60%; |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
margin-top: 0; |
||||||
|
background-color: #dfc; |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
import { component, html } from 'haunted'; |
||||||
|
import { useApolloClient, useQuery } from '@apollo/react-hooks'; |
||||||
|
import gql from 'graphql-tag'; |
||||||
|
import Css from './myapp.scss'; |
||||||
|
import './booklist'; |
||||||
|
import './bookform'; |
||||||
|
|
||||||
|
const App = () => { |
||||||
|
const cli = useApolloClient(); |
||||||
|
|
||||||
|
return html` |
||||||
|
<style>${Css.toString()}</style> |
||||||
|
<h1>Haunted Apollo Demo <span class="version">(client ${cli.version})</span></h1> |
||||||
|
<book-form></book-form> |
||||||
|
<book-list></book-list> |
||||||
|
`;
|
||||||
|
} |
||||||
|
|
||||||
|
customElements.define('my-app', component(App)); |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
import gql from "graphql-tag"; |
||||||
|
|
||||||
|
export const GetBooks = gql` |
||||||
|
query getBooks { |
||||||
|
books { |
||||||
|
id |
||||||
|
title |
||||||
|
author |
||||||
|
} |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
export const AddBook = gql` |
||||||
|
mutation addBook($title: String!, $author: String!) { |
||||||
|
addBook(title: $title, author: $author) { |
||||||
|
id |
||||||
|
title |
||||||
|
author |
||||||
|
} |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
export const DelBook = gql` |
||||||
|
mutation delBook($id: String!) { |
||||||
|
delBook(id: $id) { |
||||||
|
id |
||||||
|
} |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
@ -0,0 +1,5 @@ |
|||||||
|
// typings.d.ts
|
||||||
|
declare module '*.scss' { |
||||||
|
const content: string; |
||||||
|
export default content; |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
import * as Haunted from 'haunted'; |
||||||
|
|
||||||
|
export default { |
||||||
|
useState: Haunted.useState, |
||||||
|
useContext: Haunted.useContext, |
||||||
|
createContext: Haunted.createContext, |
||||||
|
}; |
||||||
|
|
||||||
|
export * from 'haunted'; |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "es5", |
||||||
|
"module": "commonjs", |
||||||
|
"removeComments": true, |
||||||
|
"esModuleInterop": true |
||||||
|
}, |
||||||
|
"include": ["src/server", "src/shared"] |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "es5", |
||||||
|
"module": "commonjs", |
||||||
|
"removeComments": true, |
||||||
|
"esModuleInterop": true, |
||||||
|
"sourceMap": true |
||||||
|
}, |
||||||
|
"include": ["src/web", "src/shared"] |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
const path = require("path"); |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
module: { |
||||||
|
rules: [ |
||||||
|
{ |
||||||
|
exclude: [/node_modules/], |
||||||
|
test: /\.ts$/, |
||||||
|
loader: "ts-loader", |
||||||
|
options: { configFile: "tsconfig.server.json" }, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
output: { |
||||||
|
filename: "server.js", |
||||||
|
path: path.resolve(__dirname, "dist", "server"), |
||||||
|
}, |
||||||
|
resolve: { |
||||||
|
extensions: [".ts", ".js"], |
||||||
|
}, |
||||||
|
target: "node", |
||||||
|
}; |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); |
||||||
|
const merge = require("webpack-merge"); |
||||||
|
const nodeExternals = require("webpack-node-externals"); |
||||||
|
const path = require("path"); |
||||||
|
const webpack = require("webpack"); |
||||||
|
|
||||||
|
const common = require("./webpack.server.common.js"); |
||||||
|
|
||||||
|
module.exports = merge.smart(common, { |
||||||
|
devtool: "inline-source-map", |
||||||
|
entry: [path.join(__dirname, "src/server/server-dev.ts")], |
||||||
|
externals: [ nodeExternals(), ], |
||||||
|
mode: "development", |
||||||
|
plugins: [new CleanWebpackPlugin()], |
||||||
|
node: { |
||||||
|
__dirname: true |
||||||
|
} |
||||||
|
}); |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); |
||||||
|
const merge = require("webpack-merge"); |
||||||
|
const nodeExternals = require("webpack-node-externals"); |
||||||
|
const path = require("path"); |
||||||
|
|
||||||
|
const common = require("./webpack.server.common.js"); |
||||||
|
|
||||||
|
module.exports = merge(common, { |
||||||
|
devtool: "source-map", |
||||||
|
entry: [path.join(__dirname, "src/server/server.ts")], |
||||||
|
externals: [nodeExternals()], |
||||||
|
mode: "production", |
||||||
|
plugins: [new CleanWebpackPlugin()], |
||||||
|
}); |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
const path = require("path"); |
||||||
|
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); |
||||||
|
const HtmlWebpackPlugin = require("html-webpack-plugin"); |
||||||
|
const CopyWebpackPlugin = require("copy-webpack-plugin"); |
||||||
|
const Dotenv = require('dotenv-webpack'); |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
target: "web", |
||||||
|
module: { |
||||||
|
rules: [ |
||||||
|
{ |
||||||
|
exclude: [/node_modules/], |
||||||
|
test: /\.ts$/, |
||||||
|
loader: "ts-loader", |
||||||
|
options: { configFile: "tsconfig.web.json" }, |
||||||
|
}, |
||||||
|
{ |
||||||
|
test: /\.(css|scss)$/, |
||||||
|
loader: "css-loader!sass-loader", |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
output: { |
||||||
|
filename: "bundle.[name].js", |
||||||
|
path: path.resolve(__dirname, "dist", "web"), |
||||||
|
}, |
||||||
|
resolve: { |
||||||
|
extensions: [".ts", ".js"], |
||||||
|
alias: { |
||||||
|
react: path.resolve(__dirname, "src/wrapper/apollo.js") |
||||||
|
}, |
||||||
|
}, |
||||||
|
plugins: [ |
||||||
|
new CleanWebpackPlugin(), |
||||||
|
new HtmlWebpackPlugin({ template: './src/index.ejs' }), |
||||||
|
new CopyWebpackPlugin([ |
||||||
|
'./node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js', |
||||||
|
{ from: './node_modules/@webcomponents/webcomponentsjs/bundles', to: 'bundles' }, |
||||||
|
]), |
||||||
|
new Dotenv(), |
||||||
|
], |
||||||
|
}; |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
const merge = require("webpack-merge"); |
||||||
|
const webpack = require("webpack"); |
||||||
|
|
||||||
|
const common = require("./webpack.web.common.js"); |
||||||
|
|
||||||
|
module.exports = merge.smart(common, { |
||||||
|
devtool: "inline-source-map", |
||||||
|
entry: ["webpack-hot-middleware/client?timeout=1000&reload=true", "./src/web/main.ts"], |
||||||
|
mode: "development", |
||||||
|
plugins: [ |
||||||
|
new webpack.HotModuleReplacementPlugin(), |
||||||
|
], |
||||||
|
}); |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
const merge = require("webpack-merge"); |
||||||
|
const path = require("path"); |
||||||
|
|
||||||
|
const common = require("./webpack.web.common.js"); |
||||||
|
|
||||||
|
module.exports = merge(common, { |
||||||
|
devtool: "source-map", |
||||||
|
entry: [path.join(__dirname, "src/web/main.ts")], |
||||||
|
mode: "production", |
||||||
|
}); |
||||||
Loading…
Reference in new issue