- adds `LOCAL_STORAGE_KEY` to `.env`, to allow saving/retrieving MobX store state to `localStorage` on the client
- adds `schema/schema.graphql`, with sample schema for Hacker News stories
- refactors `<StateConsumer>` as new `withStore()` HOC in `@/lib/store.ts`
- refactors `<HackerNews>` example to use the new auto-generayed `<GetHackerNewsTopStories.Component>`
- removes default exports in example components
- fixes #150 - React-Hot-Loader 4.7
- renames `@/data/state.ts` -> `@/data/store.ts` (now exports `Store` class)
- adds `autosave` and new `rehydrate` funcs in `@/lib/store.ts`, for saving and retrieving MobX state to/from `localStorage`
- refactors `@/entry/client.tsx` with new MobX store hydration/auto-save
- refactors `@/entry/server.tsx` with new MobX store
- adds auto-generated `@/graphql/index.tsx` with query and HOC for getting top HackerNews stories
- removes previous `@/lib/mobx.ts`
- fixes static runner; removes `koa2-history-api-fallback` package in favour of a local function
- adds `HOST` to `.env` and `common.host` in runner to allow binding to non-`localhost`
- adds `react-dom` alias to Webpack common config, to use `@hot-loader/react-dom`
pull/162/head
Lee Benson 4 years ago
parent 31a95bf5c6
commit a6abe69095
  1. 4
      .env
  2. 1
      .nvmrc
  3. 9
      codegen.yml
  4. 2962
      package-lock.json
  5. 69
      package.json
  6. 17
      schema/schema.graphql
  7. 27
      src/components/example/count.tsx
  8. 44
      src/components/example/hackernews.tsx
  9. 8
      src/components/example/index.tsx
  10. 4
      src/components/root.tsx
  11. 2
      src/data/store.ts
  12. 22
      src/entry/client.tsx
  13. 18
      src/entry/server.tsx
  14. 85
      src/graphql/index.tsx
  15. 44
      src/lib/mobx.tsx
  16. 55
      src/lib/store.ts
  17. 9
      src/queries/getHackerNewsTopStories.graphql
  18. 26
      src/queries/getHackerNewsTopStories.ts
  19. 41
      src/runner/app.ts
  20. 2
      src/runner/development.ts
  21. 22
      src/runner/static.ts
  22. 59
      src/webpack/client.ts
  23. 35
      src/webpack/common.ts
  24. 41
      src/webpack/server.ts
  25. 1
      types/globals.d.ts

@ -1,2 +1,4 @@
HOST=0.0.0.0
GRAPHQL=https://graphqlhub.com/graphql
WS_SUBSCRIPTIONS=0
WS_SUBSCRIPTIONS=0
LOCAL_STORAGE_KEY=reactql

@ -1 +0,0 @@
v11.8.0

@ -0,0 +1,9 @@
overwrite: true
schema: "schema/schema.graphql"
documents: "src/**/*.graphql"
generates:
src/graphql/index.tsx:
plugins:
- "typescript-common"
- "typescript-client"
- "typescript-react-apollo"

2962
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "reactql",
"version": "4.1.0",
"version": "4.2.0",
"description": "ReactQL - front-end React/GraphQL starter kit",
"main": "index.js",
"scripts": {
@ -9,6 +9,7 @@
"clean": "rimraf dist",
"dev": "cross-env RUNNER=development NODE_ENV=development ts-node index.ts",
"dev:static": "cross-env RUNNER=static NODE_ENV=development ts-node index.ts",
"gen:graphql": "gql-gen --config codegen.yml",
"production": "cross-env RUNNER=production NODE_ENV=production ts-node index.ts",
"production:clean": "npm run clean && npm run production",
"start": "npm run dev",
@ -17,8 +18,9 @@
"author": "Lee Benson <lee@leebenson.com>",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/core": "^7.3.3",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@hot-loader/react-dom": "^16.8.2",
"@types/compression-webpack-plugin": "^2.0.0",
"@types/history": "^4.7.2",
"@types/html-webpack-plugin": "^3.2.0",
@ -27,30 +29,34 @@
"@types/koa-router": "^7.0.39",
"@types/koa-send": "^4.1.1",
"@types/koa-webpack": "^5.0.1",
"@types/lodash": "^4.14.120",
"@types/lodash": "^4.14.121",
"@types/mini-css-extract-plugin": "^0.2.0",
"@types/node": "^10.12.18",
"@types/node": "^11.9.4",
"@types/ora": "^3.0.0",
"@types/prop-types": "^15.5.8",
"@types/react": "^16.7.21",
"@types/react-dom": "^16.0.11",
"@types/prop-types": "^15.5.9",
"@types/react": "^16.8.3",
"@types/react-dom": "^16.8.2",
"@types/react-helmet": "^5.0.8",
"@types/react-hot-loader": "^4.1.0",
"@types/react-router-dom": "^4.3.1",
"@types/require-from-string": "^1.2.0",
"@types/source-map-support": "^0.4.1",
"@types/source-map-support": "^0.4.2",
"@types/webpack": "^4.4.24",
"@types/webpack-dev-server": "^3.1.1",
"@types/webpack-dev-server": "^3.1.2",
"@types/webpack-node-externals": "^1.6.3",
"babel-core": "^6.26.3",
"babel-loader": "^8.0.5",
"babel-plugin-emotion": "^10.0.6",
"babel-plugin-emotion": "^10.0.7",
"brotli-webpack-plugin": "^1.1.0",
"compression-webpack-plugin": "^2.0.0",
"css-hot-loader": "^1.4.3",
"css-loader": "^2.1.0",
"cssnano": "^4.1.8",
"cssnano": "^4.1.10",
"file-loader": "^3.0.1",
"graphql-code-generator": "^0.16.1",
"graphql-codegen-typescript-client": "^0.16.1",
"graphql-codegen-typescript-common": "^0.16.1",
"graphql-codegen-typescript-react-apollo": "^0.16.1",
"html-webpack-plugin": "^3.2.0",
"koa-webpack": "^5.2.1",
"less": "^3.9.0",
@ -60,53 +66,52 @@
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.5.0",
"require-from-string": "^2.0.2",
"resolve-url-loader": "^3.0.0",
"resolve-url-loader": "^3.0.1",
"rimraf": "^2.6.3",
"sass-loader": "^7.1.0",
"source-map-support": "^0.5.10",
"ts-loader": "^5.3.3",
"ts-node": "^8.0.2",
"tslint": "^5.12.1",
"typescript": "^3.2.4",
"webpack": "^4.29.0",
"typescript": "^3.3.3",
"webpack": "^4.29.5",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"@emotion/core": "^10.0.6",
"@emotion/styled": "^10.0.6",
"apollo-cache-inmemory": "^1.4.2",
"apollo-client": "^2.4.12",
"apollo-link": "^1.2.6",
"apollo-link-error": "^1.1.5",
"apollo-link-http": "^1.5.9",
"@emotion/core": "^10.0.7",
"@emotion/styled": "^10.0.7",
"apollo-cache-inmemory": "^1.4.3",
"apollo-client": "^2.4.13",
"apollo-link": "^1.2.8",
"apollo-link-error": "^1.1.7",
"apollo-link-http": "^1.5.11",
"apollo-link-state": "^0.4.2",
"apollo-link-ws": "^1.0.12",
"apollo-utilities": "^1.1.2",
"apollo-link-ws": "^1.0.14",
"apollo-utilities": "^1.1.3",
"chalk": "^2.4.2",
"cross-env": "^5.2.0",
"cross-fetch": "^3.0.0",
"cross-fetch": "^3.0.1",
"dotenv": "^6.2.0",
"emotion": "^10.0.6",
"emotion": "^10.0.7",
"graphql": "^14.1.1",
"graphql-tag": "^2.10.1",
"history": "^4.7.2",
"kcors": "^2.2.2",
"koa": "^2.6.2",
"koa": "^2.7.0",
"koa-router": "^7.4.0",
"koa-send": "^5.0.0",
"koa2-history-api-fallback": "0.0.5",
"lodash": "^4.17.11",
"microseconds": "^0.1.0",
"mobx": "^4.9.2",
"mobx-react": "^5.4.3",
"ora": "^3.0.0",
"react": "^16.7.0",
"ora": "^3.1.0",
"react": "^16.8.2",
"react-addons-css-transition-group": "^15.6.2",
"react-addons-transition-group": "^15.6.2",
"react-apollo": "^2.4.0",
"react-dom": "^16.7.0",
"react-apollo": "^2.4.1",
"react-dom": "^16.8.2",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.6.3",
"react-hot-loader": "^4.7.0",
"react-router-dom": "^4.3.1",
"subscriptions-transport-ws": "^0.9.15"
}

@ -0,0 +1,17 @@
type Story {
id: String
title: String
url: String
}
type HackerNews {
topStories: [Story]
}
type Query {
hn: HackerNews
}
schema {
query: Query
}

@ -8,24 +8,15 @@ import * as React from "react";
/* Local */
// `<StateConsumer>` takes a function and passes it our MobX
// state. Any time the state changes, the children will automatically
// re-render -- no HOCs or boilerplate required!
import { StateConsumer } from "@/lib/mobx";
// `withStore` gives us access to MobX store state
import { withStore } from "@/lib/store";
// ----------------------------------------------------------------------------
export default class Count extends React.Component {
public render() {
return (
<StateConsumer>
{state => (
<>
<h3>Current count (from MobX): {state.count}</h3>
<button onClick={state.increment}>Increment</button>
</>
)}
</StateConsumer>
);
}
}
export const Count = withStore(({ store }) => (
<>
<h3>Current count (from MobX): {store.count}</h3>
<button onClick={store.increment}>Increment</button>
<button onClick={() => (store.count = 0)}>Reset</button>
</>
));

@ -6,36 +6,16 @@
/* NPM */
import * as React from "react";
// Use the `<Query>` component from the React Apollo lib to declaratively
// fetch the GraphQL data, to display as part of our component
import { Query } from "react-apollo";
// Emotion styled component
import styled from "@emotion/styled";
/* Local */
// Query to get top stories from HackerNews
import hackerNewsQuery from "@/queries/getHackerNewsTopStories";
import { GetHackerNewsTopStories } from "@/graphql";
// ----------------------------------------------------------------------------
// Typescript types
// Represents a HackerNews story - id, title and url
interface IHackerNewsStory {
id: string;
title: string;
url: string;
}
// Represents the data returned by the Hacker News GraphQL query
interface IHackerNewsTopStories {
hn: {
topStories: IHackerNewsStory[];
};
}
// Unstyled Emotion parent block, to avoid repeating <style> tags
// on child elements -- see https://github.com/emotion-js/emotion/issues/1061
const List = styled.ul``;
@ -55,17 +35,17 @@ const Story = styled("li")`
// whatever the server has sent it - or, if it's a client-navigated route that
// doesn't already have data from the server -- it'll display a loading message
// while the data is being retrieved
export default () => (
<Query<IHackerNewsTopStories> query={hackerNewsQuery}>
{result => {
export const HackerNews: React.FunctionComponent = () => (
<GetHackerNewsTopStories.Component>
{({ data, loading, error }) => {
// Any errors? Say so!
if (result.error) {
return <h1>Error retrieving news stories! &mdash; {result.error}</h1>;
if (error) {
return <h1>Error retrieving news stories! &mdash; {error}</h1>;
}
// If the data is still loading, return with a basic
// message to alert the user
if (result.loading) {
if (loading) {
return <h1>Loading Hacker News stories...</h1>;
}
@ -75,10 +55,10 @@ export default () => (
<>
<h3>Top stories from Hacker News</h3>
<List>
{result.data!.hn.topStories.map(story => (
<Story key={story.id}>
<a href={story.url} target="_blank">
{story.title}
{data!.hn!.topStories!.map(story => (
<Story key={story!.id!}>
<a href={story!.url!} target="_blank">
{story!.title}
</a>
</Story>
))}
@ -86,5 +66,5 @@ export default () => (
</>
);
}}
</Query>
</GetHackerNewsTopStories.Component>
);

@ -10,10 +10,10 @@ import * as React from "react";
/* Local */
// Counter, controlled by local Apollo state
import Count from "./count";
import { Count } from "./count";
// Hacker News GraphQL example
import HackerNews from "./hackernews";
import { HackerNews } from "./hackernews";
// ----------------------------------------------------------------------------
@ -24,7 +24,7 @@ interface IIndexState {
// Say hello from GraphQL, along with a HackerNews feed fetched by GraphQL
class Index extends React.PureComponent<{}, IIndexState> {
public state = {
dynamic: null
dynamic: null,
};
public componentDidMount = async () => {
@ -33,7 +33,7 @@ class Index extends React.PureComponent<{}, IIndexState> {
// ... and keep ahold of it locally
this.setState({
dynamic: dynamic.default
dynamic: dynamic.default,
});
};

@ -6,7 +6,7 @@
/* NPM */
import * as React from "react";
import Helmet from "react-helmet";
import { hot } from "react-hot-loader";
import { hot } from "react-hot-loader/root";
import { Route, Switch } from "react-router-dom";
import { Global } from "@emotion/core";
@ -39,4 +39,4 @@ const Root = () => (
</div>
);
export default hot(module)(Root);
export default hot(Root);

@ -1,6 +1,6 @@
import { action, observable } from "mobx";
export class State {
export class Store {
@observable count = 0;
@action public increment = () => {
this.count = this.count + 1;

@ -20,6 +20,9 @@ import * as ReactDOM from "react-dom";
// Single page app routing
import { Router } from "react-router-dom";
// MobX provider
import { Provider } from "mobx-react";
/* Local */
// Our main component, and the starting point for server/browser loading
@ -29,9 +32,8 @@ import Root from "@/components/root";
import { createClient } from "@/lib/apollo";
// MobX state
import { State } from "@/data/state";
import { rehydrate, StateProvider } from "@/lib/mobx";
import { resolvePtr } from "dns";
import { Store } from "@/data/store";
import { rehydrate, autosave } from "@/lib/store";
// ----------------------------------------------------------------------------
@ -39,23 +41,23 @@ import { resolvePtr } from "dns";
const client = createClient();
// Create new MobX state
const state = new State();
const store = ((window as any).store = new Store());
// Create a browser history
const history = createBrowserHistory();
// Rehydrate MobX state, if applicable
rehydrate(state);
// Render
const root = document.getElementById("root")!;
ReactDOM[root.innerHTML ? "hydrate" : "render"](
<StateProvider value={state}>
<Provider store={store}>
<ApolloProvider client={client}>
<Router history={history}>
<Root />
</Router>
</ApolloProvider>
</StateProvider>,
document.getElementById("root")
</Provider>,
document.getElementById("root"),
);
// Rehydrate MobX store and save changes
[rehydrate, autosave].forEach(fn => fn(store));

@ -30,6 +30,9 @@ import Helmet from "react-helmet";
// React SSR routers
import { StaticRouter } from "react-router";
// MobX provider
import { Provider } from "mobx-react";
/* Local */
// Root component
@ -39,10 +42,7 @@ import Root from "@/components/root";
import { createClient } from "@/lib/apollo";
// State class, containing all of our user-land state fields
import { State } from "@/data/state";
// <StateProvider> lets us send per-request state down a React chain
import { StateProvider } from "@/lib/mobx";
import { Store } from "@/data/store";
// Class for handling Webpack stats output
import Output from "@/lib/output";
@ -67,7 +67,7 @@ export default function(output: Output) {
const client = createClient();
// Create new MobX state
const state = new State();
const store = new Store();
// Create a fresh 'context' for React Router
const routerContext: IRouterContext = {};
@ -75,13 +75,13 @@ export default function(output: Output) {
// Render our components - passing down MobX state, a GraphQL client,
// and a router for rendering based on our route config
const components = (
<StateProvider value={state}>
<Provider store={store}>
<ApolloProvider client={client}>
<StaticRouter location={ctx.request.url} context={routerContext}>
<Root />
</StaticRouter>
</ApolloProvider>
</StateProvider>
</Provider>
);
// Await GraphQL data coming from the API server
@ -124,9 +124,9 @@ export default function(output: Output) {
scripts={output.client.scripts()}
window={{
__APOLLO_STATE__: client.extract(), // <-- GraphQL store
__STATE__: toJS(state) // <-- MobX state
__STORE__: toJS(store), // <-- MobX state
}}
/>
/>,
);
// Set the return type to `text/html`, and dump the response back to

@ -0,0 +1,85 @@
export type Maybe<T> = T | null;
// ====================================================
// Documents
// ====================================================
export namespace GetHackerNewsTopStories {
export type Variables = {};
export type Query = {
__typename?: "Query";
hn: Maybe<Hn>;
};
export type Hn = {
__typename?: "HackerNews";
topStories: Maybe<(Maybe<TopStories>)[]>;
};
export type TopStories = {
__typename?: "Story";
id: Maybe<string>;
title: Maybe<string>;
url: Maybe<string>;
};
}
import * as ReactApollo from "react-apollo";
import * as React from "react";
import gql from "graphql-tag";
// ====================================================
// Components
// ====================================================
export namespace GetHackerNewsTopStories {
export const Document = gql`
query GetHackerNewsTopStories {
hn {
topStories {
id
title
url
}
}
}
`;
export class Component extends React.Component<
Partial<ReactApollo.QueryProps<Query, Variables>>
> {
render() {
return (
<ReactApollo.Query<Query, Variables>
query={Document}
{...(this as any)["props"] as any}
/>
);
}
}
export type Props<TChildProps = any> = Partial<
ReactApollo.DataProps<Query, Variables>
> &
TChildProps;
export function HOC<TProps, TChildProps = any>(
operationOptions:
| ReactApollo.OperationOption<
TProps,
Query,
Variables,
Props<TChildProps>
>
| undefined
) {
return ReactApollo.graphql<TProps, Query, Variables, Props<TChildProps>>(
Document,
operationOptions
);
}
}

@ -1,44 +0,0 @@
// MobX helpers
// ----------------------------------------------------------------------------
// IMPORTS
import * as React from "react";
import { runInAction } from "mobx";
import { Observer } from "mobx-react";
/* Local */
import { State } from "@/data/state";
// ----------------------------------------------------------------------------
interface IStateConsumerProps {
children: (state: State) => React.ReactElement<any>;
}
// State context, for propogating state down a React chain
const StateContext = React.createContext<State>(new State());
// Export a <StateProvider>, for overridding default state
export const { Provider: StateProvider } = StateContext;
// State HOC for both receiving and observing state
export const StateConsumer: React.StatelessComponent<IStateConsumerProps> = ({
children
}) => (
<StateContext.Consumer>
{state => {
return <Observer>{() => children(state)}</Observer>;
}}
</StateContext.Consumer>
);
// Rehydrate JSON state to MobX
export function rehydrate(state: State) {
const s = (window as any).__STATE__;
if (typeof s === "object") {
runInAction(() => {
Object.keys(s).forEach(key => ((state as any)[key] = s[key]));
});
}
}

@ -0,0 +1,55 @@
// MobX helpers
// ----------------------------------------------------------------------------
// IMPORTS
import * as React from "react";
import { runInAction, autorun, toJS } from "mobx";
import { observer, inject } from "mobx-react";
/* Local */
import { Store } from "@/data/store";
// ----------------------------------------------------------------------------
export type WithStore<T> = T & {
store: Store;
};
export function withStore<P>(
Component: React.ComponentType<WithStore<P>>,
): React.ComponentType<P> {
return inject<{ store: Store }, {}, {}, {}>(stores => ({
store: stores.store,
}))(observer(Component as any));
}
// CLIENT-ONLY: Rehydrate JSON state to MobX
export function rehydrate(store: Store) {
// Helper to parse and rehydrate the store
const init = (data: any) => {
if (typeof data === "object") {
Object.keys(data).forEach(key => ((store as any)[key] = data[key]));
}
};
// Perform the rehydration atomically
runInAction(() => {
init((window as any).__STORE__);
try {
const parsed = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)!);
if (parsed) {
init(parsed);
}
} catch (_) {
/* Ignore localStorage parse errors */
}
});
}
// CLIENT-ONLY: Save store changes to localStorage
export function autosave(store: Store) {
autorun(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(toJS(store)));
});
}

@ -0,0 +1,9 @@
query GetHackerNewsTopStories {
hn {
topStories {
id
title
url
}
}
}

@ -1,26 +0,0 @@
// Get the top stories from HackerNews
// ----------------------------------------------------------------------------
// IMPORTS
/* NPM */
// GraphQL tag library, for creating GraphQL queries from plain
// template text
import gql from "graphql-tag";
// ----------------------------------------------------------------------------
// GraphQL query for retrieving Hacker News top-stories from the
// https://graphqlhub.com/playground sample server endpoint
export default gql`
{
hn {
topStories {
id
title
url
}
}
}
`;

@ -53,7 +53,7 @@ function staticMiddleware(root: string, immutable = true): Koa.Middleware {
// If we're in production, try <dist>/public first
return await koaSend(ctx, ctx.path, {
immutable,
root
root,
});
}
} catch (e) {
@ -71,7 +71,7 @@ export const common = {
compiled: {
clientStats: path.resolve(dist, "client.stats.json"),
server: path.resolve(dist, "server.js"),
serverStats: path.resolve(dist, "server.stats.json")
serverStats: path.resolve(dist, "server.stats.json"),
},
// Distribution folder
@ -80,11 +80,18 @@ export const common = {
// Are we in production?
isProduction: process.env.NODE_ENV === "production",
// Host to bind the server to
host: process.env.HOST || "0.0.0.0",
// Port to start web server on
port: process.env.PORT || 3000,
port: (process.env.PORT && parseInt(process.env.PORT)) || 3000,
// WebSocket port (for dev)
websocketPort:
(process.env.WS_PORT && parseInt(process.env.WS_PORT)) || undefined,
// Spinner
spinner: ora() as any
spinner: ora() as any,
};
// Webpack compiler
@ -118,9 +125,9 @@ export function build(buildStatic = false) {
[common.compiled.serverStats, common.compiled.clientStats].forEach(
(file, i) => {
fs.writeFileSync(file, JSON.stringify(stats.children[i]), {
encoding: "utf8"
encoding: "utf8",
});
}
},
);
}
@ -133,16 +140,27 @@ export function build(buildStatic = false) {
export async function devServer(
koaApp: Koa,
compiler: webpack.MultiCompiler,
opt: PartialAll<KoaWebpack.Options> = {}
opt: PartialAll<KoaWebpack.Options> = {},
) {
// Set hot client options
const hotClient: any = {
host: common.host,
};
// Is a custom WebSocket defined?
if (common.websocketPort) {
hotClient.port = common.websocketPort;
}
// Set default options for KoaWebpack
const defaultOptions: KoaWebpack.Options = {
compiler: compiler as any,
devMiddleware: {
logLevel: "info",
publicPath: "/",
stats: false
}
stats: false,
},
hotClient,
};
// Create the middlware, by merging in any overrides
@ -153,8 +171,11 @@ export async function devServer(
// Emit the listener when Webpack has finished bundling
(compiler as any).hooks.done.tap("built", () => {
common.spinner.succeed(`Running on http://localhost:${common.port}`);
common.spinner.succeed(`Running on http://${common.host}:${common.port}`);
});
// Return the middleware
return koaWebpackMiddleware;
}
// Router

@ -16,7 +16,7 @@ common.spinner
.info(chalk.default.magenta("Development mode"))
.info("Building development server...");
app.listen({ port: common.port, host: "localhost" }, async () => {
app.listen({ port: common.port, host: common.host }, async () => {
await devServer(app, compiler);
app.use(hotServerMiddleware(compiler));
});

@ -3,13 +3,15 @@
// ----------------------------------------------------------------------------
// IMPORTS
/* Node */
import * as path from "path";
/* NPM */
import * as chalk from "chalk";
const historyFallback = require("koa2-history-api-fallback");
/* Local */
// import staticMiddleware from "../lib/staticMiddleware";
import { build, common, app, staticCompiler, devServer } from "./app";
import clientConfig from "../webpack/client";
// ----------------------------------------------------------------------------
@ -27,11 +29,17 @@ void (async () => {
// Development...
common.spinner.info("Building development server...");
app.listen({ port: common.port, host: "localhost" }, async () => {
// Fallback to /index.html on 404 routes, for client-side SPAs
app.use(historyFallback());
app.listen({ port: common.port, host: common.host }, async () => {
// Build the static dev server
await devServer(app, staticCompiler);
const middleware = await devServer(app, staticCompiler);
// Fallback to /index.html on 404 routes, for client-side SPAs
app.use(async ctx => {
const filename = path.resolve(clientConfig.output.path, "index.html");
ctx.response.type = "html";
ctx.response.body = middleware.devMiddleware.fileSystem.createReadStream(
filename,
);
});
});
})();

@ -41,10 +41,10 @@ const base: webpack.Configuration = {
{
loader: "file-loader",
query: {
name: `assets/img/[name]${isProduction ? ".[hash]" : ""}.[ext]`
}
}
]
name: `assets/img/[name]${isProduction ? ".[hash]" : ""}.[ext]`,
},
},
],
},
// Fonts
@ -54,12 +54,12 @@ const base: webpack.Configuration = {
{
loader: "file-loader",
query: {
name: `assets/fonts/[name]${isProduction ? ".[hash]" : ""}.[ext]`
}
}
]
}
]
name: `assets/fonts/[name]${isProduction ? ".[hash]" : ""}.[ext]`,
},
},
],
},
],
},
// Set-up some common mocks/polyfills for features available in node, so
@ -68,13 +68,13 @@ const base: webpack.Configuration = {
console: true,
fs: "empty",
net: "empty",
tls: "empty"
tls: "empty",
},
// Output
output: {
path: path.resolve(__dirname, "..", "..", "dist", "public"),
publicPath: "/"
publicPath: "/",
},
// The client bundle will be responsible for building the resulting
@ -87,27 +87,28 @@ const base: webpack.Configuration = {
enforce: true,
name: "main",
test: new RegExp(
`\\.${rules.map(rule => `(${rule.ext})`).join("|")}$`
)
}
}
}
`\\.${rules.map(rule => `(${rule.ext})`).join("|")}$`,
),
},
},
},
},
// Add `MiniCssExtractPlugin`
plugins: [
new MiniCssExtractPlugin({
chunkFilename: "assets/css/[id].css",
filename: `assets/css/[name]${isProduction ? ".[contenthash]" : ""}.css`
filename: `assets/css/[name]${isProduction ? ".[contenthash]" : ""}.css`,
}),
// Add global variables
new webpack.DefinePlugin({
GRAPHQL: JSON.stringify(process.env.GRAPHQL),
SERVER: false,
WS_SUBSCRIPTIONS: process.env.WS_SUBSCRIPTIONS
})
]
WS_SUBSCRIPTIONS: process.env.WS_SUBSCRIPTIONS,
LOCAL_STORAGE_KEY: JSON.stringify(process.env.LOCAL_STORAGE_KEY),
}),
],
};
// Development client config
@ -117,8 +118,8 @@ const dev: webpack.Configuration = {
// Output
output: {
chunkFilename: "[name].js",
filename: "[name].js"
}
filename: "[name].js",
},
};
// Production client config
@ -126,18 +127,18 @@ const prod: webpack.Configuration = {
// Output
output: {
chunkFilename: "assets/js/[name].[chunkhash].js",
filename: "assets/js/[name].[chunkhash].js"
filename: "assets/js/[name].[chunkhash].js",
},
plugins: [
new CompressionPlugin({
cache: true,
minRatio: 0.99
minRatio: 0.99,
}),
new BrotliCompression({
minRatio: 0.99
})
]
minRatio: 0.99,
}),
],
};
export default mergeWith(
@ -145,5 +146,5 @@ export default mergeWith(
common(false),
base,
process.env.NODE_ENV === "production" ? prod : dev,
defaultMerger
defaultMerger,
);

@ -21,13 +21,13 @@ export function defaultMerger(
key: any,
_object: any,
_source: any,
_stack: any
_stack: any,
) {
// Merge rules
if (key === "rules" && [obj, src].every(v => Array.isArray(v))) {
src.forEach((v: webpack.Rule, _i: number) => {
const existingTest = (obj as webpack.Rule[]).find(
rule => String(rule.test) === String(v.test)
rule => String(rule.test) === String(v.test),
);
if (existingTest) {
@ -51,7 +51,7 @@ const isProduction = process.env.NODE_ENV === "production";
// RegExp for file types
export const files = {
fonts: /\.(woff|woff2|(o|t)tf|eot)$/i,
images: /\.(jpe?g|png|gif|svg)$/i
images: /\.(jpe?g|png|gif|svg)$/i,
};
// Common config
@ -72,39 +72,40 @@ export default (_ssr: boolean /* <-- not currently used */) => {
plugins: [
"@babel/plugin-syntax-dynamic-import",
"react-hot-loader/babel",
"emotion"
]
}
"emotion",
],
},
},
{
loader: "ts-loader",
options: {
compilerOptions: {
module: "esnext"
module: "esnext",
},
// Avoid typechecking, to speed up bundling. To avoid the
// complexity of type checking *both* @launch/app and a project's
// `tsconfig.json`, this should be a userland exercise
transpileOnly: true
}
}
]
}
]
transpileOnly: true,
},
},
],
},
],
},
output: {
publicPath: "/"
publicPath: "/",
},
resolve: {
alias: {
"@": path.resolve(root, "src")
"@": path.resolve(root, "src"),
"react-dom": "@hot-loader/react-dom",
},
extensions: [".mjs", ".ts", ".tsx", ".jsx", ".js", ".json"],
modules: [path.resolve(root, "node_modules")]
}
modules: [path.resolve(root, "node_modules")],
},
};
return common;

@ -37,10 +37,10 @@ const base: webpack.Configuration = {
loader: "file-loader",
query: {
emitFile: false,
name: `assets/img/[name]${isProduction ? ".[hash]" : ""}.[ext]`
}
}
]
name: `assets/img/[name]${isProduction ? ".[hash]" : ""}.[ext]`,
},
},
],
},
// Fonts
@ -51,12 +51,12 @@ const base: webpack.Configuration = {
loader: "file-loader",
query: {
emitFile: false,
name: `assets/fonts/[name]${isProduction ? ".[hash]" : ""}.[ext]`
}
}
]
}
]
name: `assets/fonts/[name]${isProduction ? ".[hash]" : ""}.[ext]`,
},
},
],
},
],
},
// Name
@ -67,14 +67,14 @@ const base: webpack.Configuration = {
filename: "../server.js",
libraryTarget: "commonjs2",
path: path.resolve(__dirname, "..", "..", "dist", "public"),
publicPath: "/"
publicPath: "/",
},
// Plugins
plugins: [
// Only emit a single `server.js` chunk
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
maxChunks: 1,
}),
// Add source map support to the server-side bundle
@ -82,33 +82,34 @@ const base: webpack.Configuration = {
banner: `require("source-map-support").install();`,
entryOnly: false,
include: ["server.js"],
raw: true
raw: true,
}),
// Add global variables
new webpack.DefinePlugin({
GRAPHQL: JSON.stringify(process.env.GRAPHQL),
SERVER: true,
WS_SUBSCRIPTIONS: JSON.stringify(process.env.WS_SUBSCRIPTIONS)
})
WS_SUBSCRIPTIONS: JSON.stringify(process.env.WS_SUBSCRIPTIONS),
LOCAL_STORAGE_KEY: JSON.stringify(process.env.LOCAL_STORAGE_KEY),
}),
],
resolve: {
modules: [path.resolve(__dirname, "..", "..", "node_modules")]
modules: [path.resolve(__dirname, "..", "..", "node_modules")],
},
// Target
target: "node"
target: "node",
};
// Development config
const dev: webpack.Configuration = {
devtool: "eval-source-map"
devtool: "eval-source-map",
};
// Production config
const prod: webpack.Configuration = {
devtool: "source-map"
devtool: "source-map",
};
export default mergeWith(
@ -116,5 +117,5 @@ export default mergeWith(
common(true),
base,
process.env.NODE_ENV === "production" ? prod : dev,
defaultMerger
defaultMerger,
);

@ -1,3 +1,4 @@
declare var GRAPHQL: string;
declare var SERVER: boolean;
declare var WS_SUBSCRIPTIONS: boolean;
declare var LOCAL_STORAGE_KEY: string;

Loading…
Cancel
Save