├── .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(); 23 | 24 | export default class Output { 25 | // -------------------------------------------------------------------------- 26 | /* PUBLIC METHODS */ 27 | // -------------------------------------------------------------------------- 28 | 29 | /* CONSTRUCTOR */ 30 | public constructor(c: IOutput) { 31 | config.set(this, c); 32 | } 33 | 34 | /* GETTERS */ 35 | 36 | // Return the Webpack client build stats 37 | public get client() { 38 | return config.get(this)!.client; 39 | } 40 | 41 | // Return the Webpack server build stats 42 | public get server() { 43 | return config.get(this)!.server; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/root.tsx: -------------------------------------------------------------------------------- 1 | // Root entry point 2 | 3 | // ---------------------------------------------------------------------------- 4 | // IMPORTS 5 | 6 | /* NPM */ 7 | import React from "react"; 8 | import Helmet from "react-helmet"; 9 | import { hot } from "react-hot-loader/root"; 10 | import { Route, Switch } from "react-router-dom"; 11 | import { Global } from "@emotion/core"; 12 | 13 | /* Local */ 14 | 15 | // Components 16 | import ScrollTop from "@/components/helpers/scrollTop"; 17 | 18 | // Global styles 19 | import globalStyles from "@/global/styles"; 20 | 21 | // By default, pull in the ReactQL example. In your own project, just nix 22 | // the `src/components/example` folder and replace the following line with 23 | // your own React components 24 | import Example from "@/components/example"; 25 | 26 | // ---------------------------------------------------------------------------- 27 | 28 | const Root: React.FunctionComponent = () => ( 29 |
30 | 31 | 32 | ReactQL starter kit - edit me! 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | 42 | export default hot(Root); 43 | -------------------------------------------------------------------------------- /src/entry/client.tsx: -------------------------------------------------------------------------------- 1 | // Client entry point 2 | 3 | // ---------------------------------------------------------------------------- 4 | // IMPORTS 5 | 6 | /* NPM */ 7 | 8 | // Create browser history, for navigation a la single page apps 9 | import { createBrowserHistory } from "history"; 10 | 11 | // React, our UI engine 12 | import React from "react"; 13 | 14 | // HOC for enabling Apollo GraphQL `` and `` 15 | import { ApolloProvider } from "react-apollo"; 16 | 17 | // Attach React to the browser DOM 18 | import ReactDOM from "react-dom"; 19 | 20 | // Single page app routing 21 | import { Router } from "react-router-dom"; 22 | 23 | /* Local */ 24 | 25 | // Our main component, and the starting point for server/browser loading 26 | import Root from "@/components/root"; 27 | 28 | // Helper function that creates a new Apollo client per request 29 | import { createClient } from "@/lib/apollo"; 30 | 31 | // ---------------------------------------------------------------------------- 32 | 33 | // Create Apollo client 34 | const client = createClient(); 35 | 36 | // Create a browser history 37 | const history = createBrowserHistory(); 38 | 39 | // Render 40 | const root = document.getElementById("root")!; 41 | ReactDOM[root.innerHTML ? "hydrate" : "render"]( 42 | 43 | 44 | 45 | 46 | , 47 | document.getElementById("root") 48 | ); 49 | -------------------------------------------------------------------------------- /src/runner/static.ts: -------------------------------------------------------------------------------- 1 | // Runner (static) 2 | 3 | // ---------------------------------------------------------------------------- 4 | // IMPORTS 5 | 6 | /* Node */ 7 | import path from "path"; 8 | 9 | /* NPM */ 10 | import chalk from "chalk"; 11 | 12 | /* Local */ 13 | import { build, common, app, staticCompiler, devServer } from "./app"; 14 | import clientConfig from "../webpack/client"; 15 | 16 | // ---------------------------------------------------------------------------- 17 | 18 | common.spinner.info(chalk.bgBlue("Static mode")); 19 | 20 | void (async () => { 21 | // Production? 22 | if (common.isProduction) { 23 | common.spinner.info("Building production files..."); 24 | await build(true /* build in static mode */); 25 | common.spinner.succeed("Finished building"); 26 | return; 27 | } 28 | 29 | // Development... 30 | common.spinner.info("Building development server..."); 31 | 32 | app.listen({ port: common.port, host: common.host }, async () => { 33 | // Build the static dev server 34 | const middleware = await devServer(app, staticCompiler); 35 | 36 | // Fallback to /index.html on 404 routes, for client-side SPAs 37 | app.use(async ctx => { 38 | const filename = path.resolve(clientConfig.output.path, "index.html"); 39 | ctx.response.type = "html"; 40 | ctx.response.body = middleware.devMiddleware.fileSystem.createReadStream( 41 | filename 42 | ); 43 | }); 44 | }); 45 | })(); 46 | -------------------------------------------------------------------------------- /src/components/example/index.tsx: -------------------------------------------------------------------------------- 1 | // ReactQL example page - delete this folder for your own project! 2 | 3 | // ---------------------------------------------------------------------------- 4 | // IMPORTS 5 | 6 | /* NPM */ 7 | 8 | import React from "react"; 9 | 10 | /* Local */ 11 | 12 | // Counter, controlled by local Apollo state 13 | import { Count } from "./count"; 14 | 15 | // Hacker News GraphQL example 16 | import { HackerNews } from "./hackernews"; 17 | 18 | // ---------------------------------------------------------------------------- 19 | 20 | interface IIndexState { 21 | dynamic: React.SFC | null; 22 | } 23 | 24 | // Say hello from GraphQL, along with a HackerNews feed fetched by GraphQL 25 | class Index extends React.PureComponent<{}, IIndexState> { 26 | public state = { 27 | dynamic: null 28 | }; 29 | 30 | public componentDidMount = async () => { 31 | // Fetch the component dynamically 32 | const dynamic = await import("./dynamic"); 33 | 34 | // ... and keep ahold of it locally 35 | this.setState({ 36 | dynamic: dynamic.default 37 | }); 38 | }; 39 | 40 | public render() { 41 | const DynamicComponent = this.state.dynamic || (() =>

Loading...

); 42 | 43 | return ( 44 | <> 45 | {/* Note: The

style will have a yellow background due to @/global/styles.ts! */} 46 |

Hi from ReactQL

47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default Index; 56 | -------------------------------------------------------------------------------- /src/runner/production.ts: -------------------------------------------------------------------------------- 1 | // Runner (production) 2 | 3 | // ---------------------------------------------------------------------------- 4 | // IMPORTS 5 | 6 | /* Node */ 7 | import fs from "fs"; 8 | 9 | /* NPM */ 10 | import chalk from "chalk"; 11 | 12 | /* Local */ 13 | import Output from "../lib/output"; 14 | import Stats, { IStats } from "../lib/stats"; 15 | import { app, build, common } from "./app"; 16 | 17 | // ---------------------------------------------------------------------------- 18 | 19 | function getStats(file: string): IStats { 20 | return JSON.parse(fs.readFileSync(file, "utf8")) as IStats; 21 | } 22 | 23 | common.spinner.info(chalk.green("Production mode")); 24 | 25 | void (async () => { 26 | // Get a list of file accessibility 27 | const files = Object.values(common.compiled).map(file => { 28 | try { 29 | fs.accessSync(file); 30 | return true; 31 | } catch (_e) { 32 | return false; 33 | } 34 | }); 35 | 36 | // Compile the server if we don't have all the expected files 37 | if (!files.every(file => file)) { 38 | common.spinner.info("Building production server..."); 39 | await build(); 40 | } else { 41 | common.spinner.info("Using cached build files"); 42 | } 43 | 44 | // Create an Output 45 | const output = new Output({ 46 | client: new Stats(getStats(common.compiled.clientStats)), 47 | server: new Stats(getStats(common.compiled.serverStats)) 48 | }); 49 | 50 | // Attach middleware 51 | app.use(require(common.compiled.server).default(output)); 52 | 53 | app.listen(common.port, () => { 54 | common.spinner.succeed(`Running on http://localhost:${common.port}`); 55 | }); 56 | })(); 57 | -------------------------------------------------------------------------------- /src/lib/stats.ts: -------------------------------------------------------------------------------- 1 | /* 2 | A `Stats` instance wraps client/server Webpack stats to provide 3 | helper functions to obtain chunk names, etc. 4 | */ 5 | 6 | // ---------------------------------------------------------------------------- 7 | // IMPORTS 8 | 9 | /* NPM */ 10 | import lodash from "lodash"; 11 | 12 | // ---------------------------------------------------------------------------- 13 | 14 | export interface IStats { 15 | assetsByChunkName?: { 16 | main: string | string[]; 17 | }; 18 | } 19 | 20 | // Config for `Stats` instances 21 | const config = new WeakMap(); 22 | 23 | export default class Stats { 24 | // -------------------------------------------------------------------------- 25 | /* PUBLIC METHODS */ 26 | // -------------------------------------------------------------------------- 27 | 28 | /* CONSTRUCTOR */ 29 | public constructor(stats: IStats = {}) { 30 | // Store a raw copy of the config 31 | config.set(this, stats); 32 | } 33 | 34 | /* GETTERS */ 35 | 36 | // Get the full, raw stats 37 | public get raw(): any { 38 | return config.get(this)!; 39 | } 40 | 41 | // Get main built asset based on file extension 42 | public main(ext: string): string | undefined { 43 | const main: string | string[] = lodash.get( 44 | config.get(this)!, 45 | "assetsByChunkName.main", 46 | [] 47 | ); 48 | const file = (Array.isArray(main) ? main : [main]).find((c: string) => 49 | c.endsWith(`.${ext}`) 50 | ); 51 | return file && `/${file}`; 52 | } 53 | 54 | public scripts(): string[] { 55 | const initial = this.raw.chunks.find((chunk: any) => chunk.initial); 56 | 57 | const scripts: string[] = initial.siblings 58 | .map((sibling: any) => 59 | this.raw.chunks.find((chunk: any) => chunk.id === sibling) 60 | ) 61 | .map((sibling: any) => sibling.files) 62 | .concat(initial.files) 63 | .flat() 64 | .filter((file: string) => file.endsWith(".js")) 65 | .map((file: string) => `/${file}`); 66 | 67 | return scripts; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/views/ssr.tsx: -------------------------------------------------------------------------------- 1 | // Server-side HTML render 2 | 3 | // Component to render the full HTML response in React 4 | 5 | // ---------------------------------------------------------------------------- 6 | // IMPORTS 7 | 8 | /* NPM */ 9 | import React from "react"; 10 | import { HelmetData } from "react-helmet"; 11 | 12 | // ---------------------------------------------------------------------------- 13 | 14 | // Types 15 | 16 | export interface IHtmlProps { 17 | css?: string; 18 | helmet: HelmetData; 19 | html: string; 20 | scripts: string[]; 21 | styles?: Array>; 22 | window?: { 23 | [key: string]: object; 24 | }; 25 | } 26 | 27 | export default class Html extends React.PureComponent { 28 | public render() { 29 | const { css, helmet, html, scripts, styles, window = {} } = this.props; 30 | return ( 31 | 36 | 37 | {helmet.title.toComponent()} 38 | 39 | 40 | 41 | 42 | {helmet.meta.toComponent()} 43 | {helmet.style.toComponent()} 44 | {helmet.link.toComponent()} 45 | {css && } 46 | {styles} 47 | {helmet.script.toComponent()} 48 | {helmet.noscript.toComponent()} 49 | 50 | 51 |
52 |