Browse Source

initial commit.

master
Anthony Hinsinger 6 years ago
commit
38c949139b
  1. 1
      .env.example
  2. 1
      .eslintignore
  3. 6
      .gitignore
  4. 80
      package.json
  5. 10
      src/index.ejs
  6. 58
      src/server/apollo.ts
  7. 19
      src/server/server-dev.ts
  8. 12
      src/server/server.ts
  9. 27
      src/web/bookform.scss
  10. 47
      src/web/bookform.ts
  11. 22
      src/web/booklist.scss
  12. 32
      src/web/booklist.ts
  13. 18
      src/web/main.ts
  14. 17
      src/web/myapp.scss
  15. 19
      src/web/myapp.ts
  16. 30
      src/web/queries.ts
  17. 5
      src/web/typings.d.ts
  18. 9
      src/wrapper/apollo.js
  19. 9
      tsconfig.server.json
  20. 10
      tsconfig.web.json
  21. 22
      webpack.server.common.js
  22. 18
      webpack.server.development.js
  23. 14
      webpack.server.production.js
  24. 42
      webpack.web.common.js
  25. 13
      webpack.web.development.js
  26. 10
      webpack.web.production.js
  27. 6758
      yarn.lock

1
.env.example

@ -0,0 +1 @@ @@ -0,0 +1 @@
BASE_URL=https://www.mydomain.com

1
.eslintignore

@ -0,0 +1 @@ @@ -0,0 +1 @@
**/*.js

6
.gitignore vendored

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
.env
dist
node_modules
yarn-error.log
.*.swp

80
package.json

@ -0,0 +1,80 @@ @@ -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
}
}
}

10
src/index.ejs

@ -0,0 +1,10 @@ @@ -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>

58
src/server/apollo.ts

@ -0,0 +1,58 @@ @@ -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;

19
src/server/server-dev.ts

@ -0,0 +1,19 @@ @@ -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');
});

12
src/server/server.ts

@ -0,0 +1,12 @@ @@ -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');
});

27
src/web/bookform.scss

@ -0,0 +1,27 @@ @@ -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;
}
}

47
src/web/bookform.ts

@ -0,0 +1,47 @@ @@ -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));

22
src/web/booklist.scss

@ -0,0 +1,22 @@ @@ -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;
}
}

32
src/web/booklist.ts

@ -0,0 +1,32 @@ @@ -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));

18
src/web/main.ts

@ -0,0 +1,18 @@ @@ -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);

17
src/web/myapp.scss

@ -0,0 +1,17 @@ @@ -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;
}

19
src/web/myapp.ts

@ -0,0 +1,19 @@ @@ -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));

30
src/web/queries.ts

@ -0,0 +1,30 @@ @@ -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
}
}
`;

5
src/web/typings.d.ts vendored

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
// typings.d.ts
declare module '*.scss' {
const content: string;
export default content;
}

9
src/wrapper/apollo.js

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
import * as Haunted from 'haunted';
export default {
useState: Haunted.useState,
useContext: Haunted.useContext,
createContext: Haunted.createContext,
};
export * from 'haunted';

9
tsconfig.server.json

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"removeComments": true,
"esModuleInterop": true
},
"include": ["src/server", "src/shared"]
}

10
tsconfig.web.json

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"removeComments": true,
"esModuleInterop": true,
"sourceMap": true
},
"include": ["src/web", "src/shared"]
}

22
webpack.server.common.js

@ -0,0 +1,22 @@ @@ -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",
};

18
webpack.server.development.js

@ -0,0 +1,18 @@ @@ -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
}
});

14
webpack.server.production.js

@ -0,0 +1,14 @@ @@ -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()],
});

42
webpack.web.common.js

@ -0,0 +1,42 @@ @@ -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(),
],
};

13
webpack.web.development.js

@ -0,0 +1,13 @@ @@ -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(),
],
});

10
webpack.web.production.js

@ -0,0 +1,10 @@ @@ -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",
});

6758
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save