initial commit.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
BASE_URL=https://www.mydomain.com
|
||||
@@ -0,0 +1 @@
|
||||
**/*.js
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Vendored
+5
@@ -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",
|
||||
});
|
||||
Reference in New Issue
Block a user