├── .nvmrc
├── .dockerignore
├── .env
├── .vscode
└── settings.json
├── netlify.toml
├── src
├── queries
│ └── getHackerNewsTopStories.graphql
├── graphql
│ ├── fragments.tsx
│ └── index.tsx
├── global
│ ├── styles.global.scss
│ └── styles.ts
├── runner
│ ├── build.ts
│ ├── development.ts
│ ├── static.ts
│ ├── production.ts
│ └── app.ts
├── components
│ ├── example
│ │ ├── dynamic.tsx
│ │ ├── count.tsx
│ │ ├── index.tsx
│ │ └── hackernews.tsx
│ ├── helpers
│ │ └── scrollTop.tsx
│ └── root.tsx
├── views
│ ├── static.html
│ └── ssr.tsx
├── webpack
│ ├── static.ts
│ ├── common.ts
│ ├── server.ts
│ ├── css.ts
│ └── client.ts
├── lib
│ ├── output.ts
│ ├── stats.ts
│ ├── apollo.ts
│ └── hotServerMiddleware.ts
└── entry
│ ├── client.tsx
│ └── server.tsx
├── types
├── global.d.ts
├── fonts.d.ts
├── microseconds.d.ts
└── images.d.ts
├── .prettierignore
├── schema
└── schema.graphql
├── codegen.yml
├── Dockerfile
├── .gitignore
├── index.ts
├── tsconfig.json
├── LICENSE
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | v12.2.0
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .git
3 | .vscode
4 | dist
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | HOST=0.0.0.0
2 | GRAPHQL=https://graphqlhub.com/graphql
3 | WS_SUBSCRIPTIONS=0
4 | LOCAL_STORAGE_KEY=reactql
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2,
3 | "typescript.tsdk": "node_modules/typescript/lib",
4 | }
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "npm run build:static"
3 | publish = "dist/public"
4 |
5 | [[redirects]]
6 | from = "/*"
7 | to = "/index.html"
8 | status = 200
9 |
--------------------------------------------------------------------------------
/src/queries/getHackerNewsTopStories.graphql:
--------------------------------------------------------------------------------
1 | query GetHackerNewsTopStories {
2 | hn {
3 | topStories {
4 | id
5 | title
6 | url
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/types/global.d.ts:
--------------------------------------------------------------------------------
1 | // Globals
2 | declare var GRAPHQL: string;
3 | declare var SERVER: boolean;
4 | declare var WS_SUBSCRIPTIONS: boolean;
5 | declare var LOCAL_STORAGE_KEY: string;
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # git
2 | .github
3 | **/.gitattributes
4 |
5 | # editors
6 | .vscode
7 | *.un~
8 | *.swp
9 |
10 | # NPM
11 | **/node_modules
12 |
13 | # ext to ignore
14 | **/*.svg
15 | **/.DS_Store
--------------------------------------------------------------------------------
/schema/schema.graphql:
--------------------------------------------------------------------------------
1 | type Story {
2 | id: String
3 | title: String
4 | url: String
5 | }
6 |
7 | type HackerNews {
8 | topStories: [Story]
9 | }
10 |
11 | type Query {
12 | hn: HackerNews
13 | }
14 |
15 | schema {
16 | query: Query
17 | }
18 |
--------------------------------------------------------------------------------
/types/fonts.d.ts:
--------------------------------------------------------------------------------
1 | // Fonts
2 | declare module "*.eot" {
3 | const value: string;
4 | export default value;
5 | }
6 |
7 | declare module "*.ttf" {
8 | const value: string;
9 | export default value;
10 | }
11 |
12 | declare module "*.woff" {
13 | const value: string;
14 | export default value;
15 | }
16 |
--------------------------------------------------------------------------------
/codegen.yml:
--------------------------------------------------------------------------------
1 | overwrite: true
2 | schema: "schema/schema.graphql"
3 | documents: "src/**/*.graphql"
4 | generates:
5 | src/graphql/index.tsx:
6 | plugins:
7 | - typescript
8 | - typescript-operations
9 | - typescript-react-apollo
10 | src/graphql/fragments.tsx:
11 | plugins:
12 | - fragment-matcher
13 |
--------------------------------------------------------------------------------
/src/graphql/fragments.tsx:
--------------------------------------------------------------------------------
1 | export interface IntrospectionResultData {
2 | __schema: {
3 | types: {
4 | kind: string;
5 | name: string;
6 | possibleTypes: {
7 | name: string;
8 | }[];
9 | }[];
10 | };
11 | }
12 |
13 | const result: IntrospectionResultData = {
14 | __schema: {
15 | types: []
16 | }
17 | };
18 |
19 | export default result;
20 |
--------------------------------------------------------------------------------
/types/microseconds.d.ts:
--------------------------------------------------------------------------------
1 | declare module "microseconds" {
2 | interface Parsed {
3 | microseconds: number;
4 | milliseconds: number;
5 | seconds: number;
6 | minutes: number;
7 | hours: number;
8 | days: number;
9 | toString(): string;
10 | }
11 | function now(): number;
12 | function parse(nano: number): Parsed;
13 | function since(nano: number): number;
14 | }
15 |
--------------------------------------------------------------------------------
/src/global/styles.global.scss:
--------------------------------------------------------------------------------
1 | /* Regular @import url statements work as you'd expect them to -- alternatively
2 | you can import locally, and they wind up in the resulting bundle */
3 | @import url("https://fonts.googleapis.com/css?family=Gentium+Basic");
4 |
5 | html {
6 | padding: 0;
7 | border: 0;
8 | font-family: "Gentium Basic", serif;
9 | font-size: 16px;
10 |
11 | /* We can nest rules, thanks to SASS */
12 | li {
13 | font-size: 2rem;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/types/images.d.ts:
--------------------------------------------------------------------------------
1 | // Images
2 | declare module "*.png" {
3 | const value: string;
4 | export default value;
5 | }
6 |
7 | declare module "*.jpg" {
8 | const value: string;
9 | export default value;
10 | }
11 |
12 | declare module "*.jpeg" {
13 | const value: string;
14 | export default value;
15 | }
16 |
17 | declare module "*.gif" {
18 | const value: string;
19 | export default value;
20 | }
21 |
22 | declare module "*.svg" {
23 | const value: string;
24 | export default value;
25 | }
26 |
--------------------------------------------------------------------------------
/src/runner/build.ts:
--------------------------------------------------------------------------------
1 | // Runner (production)
2 |
3 | // ----------------------------------------------------------------------------
4 | // IMPORTS
5 |
6 | /* NPM */
7 | import chalk from "chalk";
8 |
9 | /* Local */
10 | import { build, common } from "./app";
11 |
12 | // ----------------------------------------------------------------------------
13 |
14 | common.spinner.info(chalk.bgBlue("Build mode"));
15 |
16 | void (async () => {
17 | await build();
18 | common.spinner.succeed("Finished building");
19 | })();
20 |
--------------------------------------------------------------------------------
/src/components/example/dynamic.tsx:
--------------------------------------------------------------------------------
1 | // Dynamic component that's loaded by `await import("./dynamic")`
2 |
3 | // ----------------------------------------------------------------------------
4 | // IMPORTS
5 |
6 | /* NPM */
7 |
8 | import React from "react";
9 |
10 | // ----------------------------------------------------------------------------
11 |
12 | // Say hello from GraphQL, along with a HackerNews feed fetched by GraphQL
13 | const Dynamic: React.FunctionComponent = () => (
14 | <>
15 |
This component was loaded dynamically!
16 | >
17 | );
18 |
19 | export default Dynamic;
20 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12.2.0-alpine AS builder
2 |
3 | # log most things
4 | ENV NPM_CONFIG_LOGLEVEL notice
5 |
6 | # OS packages for compilation
7 | RUN apk add --no-cache python2 make g++
8 |
9 | # install NPM packages
10 | WORKDIR /build
11 | ADD package*.json ./
12 | RUN npm i
13 |
14 | # add source
15 | ADD . .
16 |
17 | # build
18 | RUN npm run build:production
19 |
20 | ########################
21 |
22 | FROM node:12.2.0-alpine
23 | WORKDIR /app
24 |
25 | # copy source + compiled `node_modules`
26 | COPY --from=builder /build .
27 |
28 | # by default, run in production mode
29 | CMD npm run production
--------------------------------------------------------------------------------
/src/runner/development.ts:
--------------------------------------------------------------------------------
1 | // Runner (development)
2 |
3 | // ----------------------------------------------------------------------------
4 | // IMPORTS
5 |
6 | /* NPM */
7 | import chalk from "chalk";
8 |
9 | /* Local */
10 | import hotServerMiddleware from "../lib/hotServerMiddleware";
11 | import { app, common, compiler, devServer } from "./app";
12 |
13 | // ----------------------------------------------------------------------------
14 |
15 | common.spinner
16 | .info(chalk.magenta("Development mode"))
17 | .info("Building development server...");
18 |
19 | app.listen({ port: common.port, host: common.host }, async () => {
20 | await devServer(app, compiler);
21 | app.use(hotServerMiddleware(compiler));
22 | });
23 |
--------------------------------------------------------------------------------
/src/views/static.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= htmlWebpackPlugin.options.title%>
8 | <% for (var css in htmlWebpackPlugin.files.css) { %>
9 |
10 | <% } %>
11 |
12 |
13 |
14 | <% for (const chunk in htmlWebpackPlugin.files.chunks) { %>
15 |
16 | <% } %>
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/helpers/scrollTop.tsx:
--------------------------------------------------------------------------------
1 | // Scroll to the top of the window
2 |
3 | // -----------------------------------------------------------------------------
4 | // IMPORTS
5 |
6 | /* NPM */
7 | import React from "react";
8 | import { RouteComponentProps, withRouter } from "react-router-dom";
9 |
10 | // -----------------------------------------------------------------------------
11 |
12 | class ScrollTop extends React.PureComponent> {
13 | public componentDidUpdate(prevProps: RouteComponentProps) {
14 | if (this.props.location !== prevProps.location) {
15 | window.scrollTo(0, 0);
16 | }
17 | }
18 |
19 | public render() {
20 | return this.props.children;
21 | }
22 | }
23 |
24 | export default withRouter(ScrollTop);
25 |
--------------------------------------------------------------------------------
/src/components/example/count.tsx:
--------------------------------------------------------------------------------
1 | // ReactQL local state counter example
2 |
3 | // ----------------------------------------------------------------------------
4 | // IMPORTS
5 |
6 | /* NPM */
7 | import React from "react";
8 | import { Observer, useObservable } from "mobx-react-lite";
9 |
10 | // ----------------------------------------------------------------------------
11 |
12 | export const Count: React.FunctionComponent = () => {
13 | const store = useObservable({ count: 0 });
14 | return (
15 | <>
16 |
17 | {() => Current count (from MobX): {store.count}
}
18 |
19 |
20 |
21 | >
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Meta
2 | **/.DS_Store
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # node-waf configuration
27 | .lock-wscript
28 |
29 | # Compiled binary addons (http://nodejs.org/api/addons.html)
30 | build/Release
31 |
32 | # Dependency directories
33 | node_modules
34 | jspm_packages
35 |
36 | # Optional npm cache directory
37 | .npm
38 |
39 | # Optional REPL history
40 | .node_repl_history
41 |
42 | # Distribution
43 | dist
44 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import * as fs from "fs";
3 |
4 | // Load env vars, for the `GRAPHQL` endpoint and anything else we need
5 | require("dotenv").config();
6 |
7 | // Catch CTRL/CMD+C interrupts cleanly
8 | const signals: NodeJS.Signals[] = [
9 | "SIGHUP",
10 | "SIGINT",
11 | "SIGQUIT",
12 | "SIGABRT",
13 | "SIGTERM"
14 | ];
15 |
16 | signals.forEach(s => process.on(s, () => process.exit(0)));
17 |
18 | // Check that we have a specified Webpack runner
19 | if (!process.env.RUNNER) {
20 | console.error("No Webpack runner specified");
21 | process.exit(1);
22 | }
23 |
24 | // Path to runner
25 | const script = path.resolve("./src/runner", `${process.env.RUNNER!}.ts`);
26 |
27 | // Check that the runner exists
28 | if (!fs.existsSync(script)) {
29 | console.error(`Runner doesn't exist: ${script}`);
30 | process.exit(1);
31 | }
32 |
33 | // Start the script
34 | require(script);
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Incremental mode
4 | "incremental": true,
5 | // Target latest version of ECMAScript.
6 | "target": "esnext",
7 | // Search under node_modules for non-relative imports.
8 | "moduleResolution": "node",
9 | // Set module format
10 | "module": "commonjs",
11 | // Process & infer types from .js files.
12 | "allowJs": true,
13 | // Don't emit; allow Babel to transform files.
14 | "noEmit": true,
15 | // Enable strictest settings like strictNullChecks & noImplicitAny.
16 | "strict": true,
17 | // Import non-ES modules as default imports.
18 | "esModuleInterop": true,
19 | // Enable React
20 | "jsx": "preserve",
21 | // Set the base path
22 | "baseUrl": ".",
23 | // Source paths
24 | "paths": {
25 | "@/*": ["src/*"],
26 | "microseconds": ["types/microseconds.d.ts"]
27 | }
28 | },
29 | "include": ["src", "types"]
30 | }
31 |
--------------------------------------------------------------------------------
/src/webpack/static.ts:
--------------------------------------------------------------------------------
1 | // Webpack (static bundling)
2 |
3 | // ----------------------------------------------------------------------------
4 | // IMPORTS
5 |
6 | /* NPM */
7 |
8 | import { mergeWith } from "lodash";
9 | import webpack from "webpack";
10 | import {} from "webpack-dev-server";
11 |
12 | // Plugin for generating `index.html` file for static hosting
13 | import HtmlWebpackPlugin from "html-webpack-plugin";
14 |
15 | /* Local */
16 |
17 | // Common config
18 | import { defaultMerger } from "./common";
19 |
20 | // Get the client-side config as a base to extend
21 | import client from "./client";
22 |
23 | // ----------------------------------------------------------------------------
24 |
25 | // Augment client-side config with HtmlWebPackPlugin
26 | const base: webpack.Configuration = {
27 | plugins: [
28 | new HtmlWebpackPlugin({
29 | inject: false,
30 | template: "src/views/static.html",
31 | title: "ReactQL app"
32 | })
33 | ]
34 | };
35 |
36 | export default mergeWith({}, client, base, defaultMerger);
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2007-2018 Lee Benson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/global/styles.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable no-unused-expression */
2 |
3 | // Global styles
4 |
5 | /*
6 | By default, this file does two things:
7 |
8 | 1. Importing `styles.global.scss` will tell Webpack to generate a `main.css`
9 | which is automatically included along with our SSR / initial HTML. This
10 | is for processing CSS through the SASS/LESS -> PostCSS pipeline.
11 |
12 | 2. It exports a global styles template which is used by Emotion to generate
13 | styles that apply to all pages.
14 | /*
15 |
16 | // ----------------------------------------------------------------------------
17 | // IMPORTS
18 |
19 | /* NPM */
20 | import { css } from "@emotion/core";
21 |
22 | /* Local */
23 |
24 | // Import global SASS styles that you want to be rendered into the
25 | // resulting `main.css` file included with the initial render. If you don't
26 | // want a CSS file to be generated, you can comment out this line
27 | import "./styles.global.scss";
28 |
29 | // ----------------------------------------------------------------------------
30 |
31 | // Global styles to apply
32 | export default css`
33 | /* Make all tags orange */
34 | h1 {
35 | background-color: orange;
36 | }
37 | `;
38 |
--------------------------------------------------------------------------------
/src/lib/output.ts:
--------------------------------------------------------------------------------
1 | /*
2 | An `Output` instance is passed through to the Webpack entry point,
3 | when is then responsible for orchestrating middleware, routes or other
4 | functions within the Webpack'd environment
5 | */
6 |
7 | // ----------------------------------------------------------------------------
8 | // IMPORTS
9 |
10 | /* Local */
11 | import Stats from "./stats";
12 |
13 | // ----------------------------------------------------------------------------
14 |
15 | // Types
16 | export interface IOutput {
17 | client: Stats;
18 | server: Stats;
19 | }
20 |
21 | // Config cache
22 | const config = new WeakMap