├── .eslintignore ├── examples ├── mjs │ ├── lib.mjs │ ├── index.js │ └── webpack.config.js ├── typescript │ ├── .gitignore │ ├── webpack.config.js │ ├── component.tsx │ ├── index.js │ ├── tsconfig.json │ ├── app.ts │ └── package.json ├── css │ ├── src │ │ ├── index.css │ │ └── index.js │ ├── node_modules │ │ └── example-package │ │ │ ├── build │ │ │ └── css │ │ │ │ └── index.css │ │ │ └── package.json │ └── webpack.config.js ├── static │ ├── public │ │ └── robots.txt │ ├── src │ │ └── index.js │ └── webpack.config.js ├── docker │ ├── src │ │ ├── index.js │ │ └── admin.js │ ├── Dockerfile │ ├── webpack.config.js │ └── config │ │ └── nginx.conf ├── minimal │ ├── index.js │ └── webpack.config.js ├── multiple-entry │ ├── src │ │ ├── bar.js │ │ └── foo.js │ ├── package.json │ └── webpack.config.js ├── relay │ ├── .graphqlconfig │ ├── .babelrc │ ├── data │ │ └── schema.graphql │ ├── webpack.config.js │ └── src │ │ └── index.js ├── custom-csp │ ├── src │ │ └── index.js │ └── webpack.config.js ├── relay-ts │ ├── .graphqlconfig │ ├── .babelrc │ ├── data │ │ └── schema.graphql │ ├── webpack.config.js │ ├── tsconfig.json │ ├── src │ │ └── index.ts │ └── package.json ├── github-graphql │ ├── src │ │ └── index.js │ ├── data │ │ └── schema.graphql │ ├── webpack.config.js │ └── .graphqlconfig ├── single-entry │ ├── src │ │ └── index.js │ └── webpack.config.js ├── postcss │ ├── postcss.config.js │ ├── node_modules │ │ └── example-package │ │ │ ├── build │ │ │ └── css │ │ │ │ └── index.css │ │ │ └── package.json │ ├── src │ │ ├── index.js │ │ └── index.css │ └── webpack.config.js └── .eslintrc.json ├── .prettierignore ├── .travis.yml ├── .gitignore ├── prettier.config.js ├── .flowconfig ├── .eslintrc.json ├── index.html ├── test.sh ├── webpack-docker-server.js ├── LICENSE ├── package.json ├── README.md └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /examples/mjs/lib.mjs: -------------------------------------------------------------------------------- 1 | export default 'world' 2 | -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | package.json 3 | -------------------------------------------------------------------------------- /examples/css/src/index.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/static/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | - "8" 5 | -------------------------------------------------------------------------------- /examples/docker/src/index.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

App

' 2 | -------------------------------------------------------------------------------- /examples/docker/src/admin.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

Admin

' 2 | -------------------------------------------------------------------------------- /examples/minimal/index.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

Hello, World!

' 2 | -------------------------------------------------------------------------------- /examples/multiple-entry/src/bar.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

Bar

' 2 | -------------------------------------------------------------------------------- /examples/multiple-entry/src/foo.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

Foo

' 2 | -------------------------------------------------------------------------------- /examples/relay/.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "data/schema.graphql" 3 | } 4 | -------------------------------------------------------------------------------- /examples/static/src/index.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

Hello, World!

' 2 | -------------------------------------------------------------------------------- /examples/custom-csp/src/index.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

Hello, World!

' 2 | -------------------------------------------------------------------------------- /examples/relay-ts/.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "data/schema.graphql" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __generated__/ 2 | dist/ 3 | node_modules/ 4 | package-lock.json 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /examples/github-graphql/src/index.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

Hello, World!

' 2 | -------------------------------------------------------------------------------- /examples/single-entry/src/index.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML = '

Hello, World!

' 2 | -------------------------------------------------------------------------------- /examples/postcss/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('precss')] 3 | } 4 | -------------------------------------------------------------------------------- /examples/relay/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["relay"], 3 | "presets": ["@babel/react"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/css/node_modules/example-package/build/css/index.css: -------------------------------------------------------------------------------- 1 | .example-button { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /examples/postcss/node_modules/example-package/build/css/index.css: -------------------------------------------------------------------------------- 1 | .example-button { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /examples/postcss/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | document.body.innerHTML = '

Hello, World!

' 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | module.exports = require('eslint-plugin-github/prettier.config') 4 | -------------------------------------------------------------------------------- /examples/mjs/index.js: -------------------------------------------------------------------------------- 1 | const subject = require('./lib') 2 | 3 | document.body.innerHTML = `

Hello, ${subject}!

` 4 | -------------------------------------------------------------------------------- /examples/postcss/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'example-package'; 2 | 3 | $red: red; 4 | 5 | h1 { 6 | color: $red; 7 | } 8 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": ["plugin:github/es6", "plugin:github/flow"] 6 | } 7 | -------------------------------------------------------------------------------- /examples/relay-ts/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["relay", { "artifactDirectory": "./src/__generated__" }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /examples/css/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import 'example-package' 3 | 4 | document.body.innerHTML = '

Hello, World!

' 5 | -------------------------------------------------------------------------------- /examples/multiple-entry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "entries": [ 4 | "src/foo.js", 5 | "src/bar.js" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /examples/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | COPY config/nginx.conf /etc/nginx/conf.d/default.conf 4 | COPY dist /usr/share/nginx/html 5 | -------------------------------------------------------------------------------- /examples/css/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /examples/mjs/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /examples/minimal/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /examples/postcss/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /examples/static/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /examples/single-entry/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /examples/typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /examples/multiple-entry/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = config 6 | -------------------------------------------------------------------------------- /examples/relay/data/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(userId: Int!): User 3 | } 4 | 5 | type User { 6 | id: ID! 7 | username: String! 8 | fullName: String 9 | } 10 | -------------------------------------------------------------------------------- /examples/relay-ts/data/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(userId: Int!): User 3 | } 4 | 5 | type User { 6 | id: ID! 7 | username: String! 8 | fullName: String 9 | } 10 | -------------------------------------------------------------------------------- /examples/typescript/component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default class MyComponent extends React.Component { 4 | render() { 5 | return
hi
6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/github-graphql/data/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | user(userId: Int!): User 3 | } 4 | 5 | type User { 6 | id: String! 7 | username: String! 8 | fullName: String 9 | } 10 | -------------------------------------------------------------------------------- /examples/typescript/index.js: -------------------------------------------------------------------------------- 1 | import Greeter from './app' 2 | import MyComponent from './component' 3 | 4 | const greeter = new Greeter('Hello world') 5 | greeter.greet() 6 | 7 | console.log(MyComponent) 8 | -------------------------------------------------------------------------------- /examples/css/node_modules/example-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-package", 3 | "version": "1.0.0", 4 | "description": "A package to use in tests", 5 | "style": "build/css/index.css" 6 | } 7 | -------------------------------------------------------------------------------- /examples/postcss/node_modules/example-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-package", 3 | "version": "1.0.0", 4 | "description": "A package to use in tests", 5 | "style": "build/css/index.css" 6 | } 7 | -------------------------------------------------------------------------------- /examples/docker/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = (env /*: string */) => config(env, {entries: ['index', 'admin']}) 6 | -------------------------------------------------------------------------------- /examples/relay-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = (env /*: string */) => config(env, {graphqlProxyPath: '/graphql'}) 6 | -------------------------------------------------------------------------------- /examples/relay/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = (env /*: string */) => config(env, {graphqlProxyPath: '/graphql'}) 6 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "jsx": "react", 7 | "esModuleInterop": true, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/github-graphql/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = (env /*: string */) => config(env, {graphqlProxyPath: '/graphql'}) 6 | -------------------------------------------------------------------------------- /examples/typescript/app.ts: -------------------------------------------------------------------------------- 1 | class Greeter { 2 | greeting: T 3 | constructor(message: T) { 4 | this.greeting = message 5 | } 6 | greet() { 7 | return this.greeting 8 | } 9 | } 10 | 11 | export default Greeter 12 | -------------------------------------------------------------------------------- /examples/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.json"], 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "flowtype/require-valid-file-annotation": "off", 8 | "no-console": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/relay-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "es2015", 5 | "strict": true, 6 | "jsx": "react", 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/github-graphql/.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "data/schema.graphql", 3 | "extensions": { 4 | "endpoints": { 5 | "production": { 6 | "url": "https://api.github.com/graphql", 7 | "headers": { 8 | "Authorization": "Bearer ${env:API_TOKEN}" 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/custom-csp/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | const config = require('../..') // require('webpack-config-github') 4 | 5 | module.exports = (env /*: string */) => { 6 | return config(env, { 7 | cspDirectives: { 8 | // Allow local fonts and Google fonts 9 | fontSrc: ["'self' https://fonts.gstatic.com"] 10 | } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /examples/relay/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {createFragmentContainer, graphql} from 'react-relay' 3 | 4 | const Hello = ({fullName}) => React.createElement('h1', null, `Hello ${fullName}`) 5 | 6 | export default createFragmentContainer(Hello, { 7 | data: graphql` 8 | fragment src_data on User { 9 | fullName 10 | } 11 | ` 12 | }) 13 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/react": "^16.7.12" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/relay-ts/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {createFragmentContainer, graphql} from 'react-relay' 3 | 4 | const Hello = ({fullName}: {fullName: string}) => React.createElement('h1', null, `Hello ${fullName}`) 5 | 6 | export default createFragmentContainer(Hello, { 7 | data: graphql` 8 | fragment src_data on User { 9 | fullName 10 | } 11 | ` 12 | }) 13 | -------------------------------------------------------------------------------- /examples/relay-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-ts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/react": "^16.7.20", 14 | "@types/react-relay": "^1.3.10", 15 | "@types/relay-runtime": "^1.3.8" 16 | }, 17 | "dependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /examples/docker/config/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | gzip_static on; 7 | 8 | root /usr/share/nginx/html; 9 | index index.html; 10 | 11 | location ~ ^\/(?[^\/\.]+) { 12 | try_files $uri /$entry.html /index.html; 13 | } 14 | } 15 | 16 | error_page 500 502 503 504 /50x.html; 17 | location = /50x.html { 18 | root /usr/share/nginx/html; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | run_webpack() { 6 | pushd "$1" 7 | if [ -f package.json ]; then 8 | npm install 9 | fi 10 | webpack 11 | webpack --env production 12 | popd 13 | } 14 | 15 | if [ $# -eq 0 ]; then 16 | for path in examples/*; do 17 | run_webpack $path 18 | done 19 | else 20 | run_webpack "examples/$1" 21 | fi 22 | 23 | ensure_dedupe() { 24 | if [ $(npm list --parseable "$1" | wc -l) -ne 1 ]; then 25 | echo "$1: duplicate versions detected" 26 | npm list "$1" 27 | exit 1 28 | fi 29 | } 30 | 31 | ensure_dedupe "graphql" 32 | ensure_dedupe "graphql-config" 33 | ensure_dedupe "relay-compiler" 34 | ensure_dedupe "webpack" 35 | -------------------------------------------------------------------------------- /webpack-docker-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // usage: webpack-docker-server 3 | // 4 | // Test production compiled assets served via Docker container. 5 | 6 | /* @flow strict */ 7 | 8 | const {spawn} = require('child_process') 9 | 10 | function run(command, args) { 11 | return new Promise((resolve, reject) => { 12 | const child = spawn(command, args, {stdio: 'inherit'}) 13 | child.on('exit', code => { 14 | if (code !== 0) { 15 | process.exit(code) 16 | reject(new Error('command failed')) 17 | } else { 18 | resolve() 19 | } 20 | }) 21 | }) 22 | } 23 | 24 | ;(async function() { 25 | const port = 8081 26 | const tagName = 'webpack-docker-server' 27 | await run('webpack', ['--env', 'production']) 28 | await run('docker', ['build', '--tag', tagName, process.cwd()]) 29 | await run('docker', ['run', '--rm', '--interactive', '--tty', '--publish', `${port}:80`, tagName]) 30 | })() 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 GitHub, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-config-github", 3 | "version": "0.13.0", 4 | "description": "An opinionated webpack config for GitHub apps.", 5 | "main": "webpack.config.js", 6 | "bin": { 7 | "webpack-docker-server": "./webpack-docker-server.js" 8 | }, 9 | "scripts": { 10 | "test": "eslint . && flow check && ./test.sh" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/github/webpack-config-github.git" 15 | }, 16 | "author": "GitHub, Inc.", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/github/webpack-config-github/issues" 20 | }, 21 | "homepage": "https://github.com/github/webpack-config-github#readme", 22 | "files": [ 23 | "index.html", 24 | "webpack-docker-server.js", 25 | "webpack.config.js" 26 | ], 27 | "engines": { 28 | "node": ">=8.9.1" 29 | }, 30 | "dependencies": { 31 | "@babel/core": "^7.1.2", 32 | "awesome-typescript-loader": "^5.2.1", 33 | "babel-loader": "^8.0.4", 34 | "clean-webpack-plugin": "^0.1.0", 35 | "compression-webpack-plugin": "^1.1.0", 36 | "content-security-policy-builder": "^2.0.0", 37 | "copy-webpack-plugin": "^4.4.0", 38 | "css-loader": "^0.28.0", 39 | "dotenv": "^5.0.0", 40 | "dotenv-webpack": "^1.5.4", 41 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 42 | "graphql": "^14.0.0", 43 | "graphql-compiler": "^2.0.0-rc.1", 44 | "graphql-config": "^2.0.1", 45 | "html-webpack-plugin": "^3.2.0", 46 | "postcss": "^7.0.6", 47 | "postcss-loader": "^2.1.0", 48 | "read-pkg": "^3.0.0", 49 | "relay-compiler": "^2.0.0-rc.1", 50 | "relay-compiler-webpack-plugin": "^2.0.0-rc.4", 51 | "relay-compiler-language-typescript": "git://github.com/koddsson/relay-compiler-language-typescript.git#dont-require-relay-runtime-string-built", 52 | "style-loader": "^0.20.0", 53 | "terser-webpack-plugin": "^1.2.1", 54 | "typescript": "^3.2.1", 55 | "webpack": "^4.29.0", 56 | "webpack-dev-server": "^3.1.14" 57 | }, 58 | "devDependencies": { 59 | "@babel/preset-react": "^7.0.0", 60 | "babel-plugin-relay": "^2.0.0-rc.1", 61 | "eslint": "^5.9.0", 62 | "eslint-plugin-github": "^1.6.0", 63 | "flow-bin": "^0.83.0", 64 | "precss": "^2.0.0", 65 | "react": "^16.2.0", 66 | "react-relay": "^2.0.0-rc.1", 67 | "webpack-cli": "^3.2.1" 68 | }, 69 | "peerDependencies": { 70 | "graphql": "^14.0.0", 71 | "webpack": "^4.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-config-github 2 | 3 | An opinionated [webpack](https://webpack.js.org/) config for GitHub apps. 4 | 5 | ## Features 6 | 7 | * Single and multiple HTML entry points 8 | * Common chunks bundle when using multiple entry points 9 | * ES6 transpilation via Babel 10 | * Source Maps 11 | * PostCSS 12 | * HTML5 History routing (in development) 13 | * GraphQL proxy (in development) 14 | * [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) 15 | * HTML and JS minification (in production) 16 | * Static gzip compression (in production) 17 | * Docker nginx deployment 18 | 19 | ## Deployment 20 | 21 | Currently targets the [Docker nginx](https://hub.docker.com/_/nginx/) deployment environment. Improved gh-pages 22 | deployment is planned in the future. 23 | 24 | ## Basic Setup 25 | 26 | ```sh 27 | $ npm install --save-dev webpack-dev-server 28 | $ npm install --save-dev webpack-config-github 29 | ``` 30 | 31 | **webpack.config.js** 32 | 33 | ```js 34 | module.exports = require('webpack-config-github') 35 | ``` 36 | 37 | **src/index.js** 38 | 39 | ```js 40 | document.body.innerHTML = '

Hello, World!

' 41 | ``` 42 | 43 | **Start development server** 44 | 45 | ```sh 46 | $ webpack-dev-server --open 47 | ``` 48 | 49 | ## Directory Structure 50 | 51 | ``` 52 | my-app 53 | ├── package.json 54 | ├── Dockerfile 55 | ├── config 56 | │ └── nginx.conf 57 | ├── .graphqlconfig 58 | ├── data 59 | │ └── schema.graphql 60 | ├── node_modules 61 | ├── public 62 | │ └── favicon.ico 63 | │ └── robots.txt 64 | └── src 65 | └── index.js 66 | └── components 67 | └── App.js 68 | └── Layout.js 69 | └── Sidebar.js 70 | ``` 71 | 72 | **Dockerfile** 73 | 74 | The currently suggested deployment target is the [Docker nginx image](https://hub.docker.com/_/nginx/). 75 | 76 | See the [example `Dockerfile`](/examples/docker/Dockerfile). 77 | 78 | **config/nginx.conf** 79 | 80 | This [example `nginx.conf`](/examples/docker/config/nginx.conf) aligns the static serving with the `webpack-dev-server`. 81 | 82 | **.graphqlconfig** 83 | 84 | Specifies the location of the GraphQL schema and target endpoints. 85 | 86 | Here is an example configuration file when targeting the GitHub GraphQL API. 87 | 88 | ```json 89 | { 90 | "schemaPath": "data/schema.graphql", 91 | "extensions": { 92 | "endpoints": { 93 | "production": { 94 | "url": "https://api.github.com/graphql", 95 | "headers": { 96 | "Authorization": "Bearer ${env:API_TOKEN}" 97 | } 98 | } 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | See the [GraphQL Configuration Format](https://github.com/graphcool/graphql-config/blob/master/specification.md) for 105 | more information. 106 | 107 | **data/schema.graphql** 108 | 109 | When using Relay, a copy of the GraphQL schema should be checked in at this location. Checking the schema in ensures 110 | linting and build tools can be consistently ran offline and in CI. 111 | 112 | **public/** 113 | 114 | The `public/` directory contains static assets that will be exposed as is. This is useful for well known static assets 115 | that need to be served at a specific root path like `favicon.ico` and `robots.txt`. 116 | 117 | **src/** 118 | 119 | Contains source JavaScript, CSS and other assets that will be compiled via webpack. 120 | 121 | **src/index.js** 122 | 123 | Is the main entry point for bootstrapping the application. 124 | 125 | When using React, this file should render the root application component. 126 | 127 | ```js 128 | import React from 'react' 129 | import ReactDOM from 'react-dom' 130 | 131 | import App from './components/App' 132 | 133 | ReactDOM.render(, document.getElementById('root')) 134 | ``` 135 | 136 | ## Roadmap 137 | 138 | * Add Subresource Integrity support 139 | * Support CSS Modules 140 | * Support hot reloading 141 | * Add gh-pages deployment pattern 142 | 143 | ## See Also 144 | 145 | Many of the directory conventions used here are inspired from 146 | [create-react-app](https://github.com/facebookincubator/create-react-app). 147 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | /* eslint eslint-comments/no-use: off */ 3 | /* eslint-disable flowtype/no-flow-fix-me-comments */ 4 | 5 | const dotenv = require('dotenv') 6 | const fs = require('fs') 7 | const path = require('path') 8 | 9 | const {getGraphQLProjectConfig} = require('graphql-config') 10 | const buildContentSecurityPolicy = require('content-security-policy-builder') 11 | const readPkg = require('read-pkg') 12 | 13 | const {EnvironmentPlugin} = require('webpack') 14 | const CleanWebpackPlugin = require('clean-webpack-plugin') 15 | const CompressionPlugin = require('compression-webpack-plugin') 16 | const CopyWebpackPlugin = require('copy-webpack-plugin') 17 | const Dotenv = require('dotenv-webpack') 18 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 19 | const HtmlWebpackPlugin = require('html-webpack-plugin') 20 | const RelayCompilerWebpackPlugin = require('relay-compiler-webpack-plugin') 21 | const RelayCompilerLanguageTypescript = require('relay-compiler-language-typescript').default 22 | const TerserPlugin = require('terser-webpack-plugin') 23 | 24 | /*:: 25 | type Options = {| 26 | allowGitHubSubresources?: boolean, 27 | commonChunkName?: string, 28 | entries?: string[], 29 | graphqlProxyPath?: string, 30 | historyApiFallback?: boolean, 31 | maxAssetSize?: number, 32 | maxEntrypointSize?: number, 33 | outputPath?: string, 34 | srcRoot?: string, 35 | staticRoot?: string, 36 | template?: string, 37 | cspDirectives?: Object 38 | |} 39 | */ 40 | 41 | /*:: 42 | type InternalOptions = {| 43 | allowGitHubSubresources: boolean, 44 | commonChunkName: string, 45 | entries: string[], 46 | graphqlProxyPath: string, 47 | historyApiFallback: boolean, 48 | maxAssetSize: number, 49 | maxEntrypointSize: number, 50 | outputPath: string, 51 | srcRoot: string, 52 | staticRoot: string, 53 | template: string, 54 | cspDirectives: Object 55 | |} 56 | */ 57 | 58 | const defaultOptions /*: InternalOptions */ = { 59 | allowGitHubSubresources: false, 60 | commonChunkName: 'common', 61 | entries: [], 62 | graphqlProxyPath: '/graphql', 63 | historyApiFallback: true, 64 | maxAssetSize: 500000, // 500 kB 65 | maxEntrypointSize: 1000000, // 1 mB 66 | outputPath: './dist', 67 | srcRoot: './src', 68 | staticRoot: './public', 69 | template: path.resolve(__dirname, 'index.html'), 70 | cspDirectives: {} 71 | } 72 | 73 | module.exports = (env /*: string */ = 'development', options /*: Options */) => { 74 | // $FlowFixMe: Forcibly cast Options to InternalOptions type after initializing default values. 75 | const opts /*: InternalOptions */ = Object.assign({}, defaultOptions, options) 76 | 77 | // Load .env file 78 | const result = dotenv.config() 79 | 80 | const cwd = process.cwd() 81 | const isDevServer = process.argv.find(v => v.includes('webpack-dev-server')) 82 | 83 | const packageJSONPath = path.resolve(cwd, 'package.json') 84 | const pkg = fs.existsSync(packageJSONPath) && readPkg.sync({path: packageJSONPath}) 85 | 86 | const config = {} 87 | 88 | config.mode = env 89 | config.optimization = {} 90 | 91 | if (env === 'production') { 92 | config.performance = { 93 | hints: 'error', 94 | maxAssetSize: opts.maxAssetSize, 95 | maxEntrypointSize: opts.maxEntrypointSize 96 | } 97 | } else { 98 | config.performance = false 99 | } 100 | 101 | config.entry = {} 102 | 103 | if (opts.entries.length > 0) { 104 | for (const name of opts.entries) { 105 | config.entry[name] = path.resolve(cwd, opts.srcRoot, `${name}.js`) 106 | } 107 | } else if (pkg) { 108 | if (pkg.main) { 109 | const name = path.basename(pkg.main, '.js') 110 | config.entry[name] = path.resolve(cwd, pkg.main) 111 | } 112 | 113 | if (pkg.entries) { 114 | for (const entry of pkg.entries) { 115 | const name = path.basename(entry, '.js') 116 | config.entry[name] = path.resolve(cwd, entry) 117 | } 118 | } 119 | } 120 | 121 | if (Object.keys(config.entry).length === 0) { 122 | const srcIndexPath = path.resolve(cwd, opts.srcRoot, `index.js`) 123 | const rootIndexPath = path.resolve(cwd, 'index.js') 124 | 125 | if (fs.existsSync(srcIndexPath)) { 126 | config.entry.index = srcIndexPath 127 | } else if (fs.existsSync(rootIndexPath)) { 128 | config.entry.index = rootIndexPath 129 | } 130 | } 131 | 132 | config.output = {} 133 | config.output.filename = '[name].[chunkhash].js' 134 | config.output.path = path.resolve(cwd, opts.outputPath) 135 | 136 | config.node = { 137 | __filename: true, 138 | __dirname: true 139 | } 140 | 141 | if (opts.historyApiFallback) { 142 | config.output.publicPath = '/' 143 | } 144 | 145 | // TODO: Fix source-map option in production environment 146 | config.devtool = env === 'production' ? 'cheap-source-map' : 'inline-source-map' 147 | 148 | if (isDevServer) { 149 | config.devServer = {} 150 | 151 | if (process.env.HOST) config.devServer.host = process.env.HOST 152 | if (process.env.PORT) config.devServer.port = process.env.PORT 153 | 154 | if (opts.historyApiFallback) { 155 | // $FlowFixMe 156 | const rewrites = Object.keys(config.entry).map(entry => { 157 | return {from: `/${entry}`, to: `/${entry}.html`} 158 | }) 159 | config.devServer.historyApiFallback = {rewrites} 160 | } 161 | 162 | config.devServer.proxy = proxyConfig(opts.graphqlProxyPath) 163 | } 164 | 165 | config.plugins = [] 166 | 167 | config.plugins.push( 168 | new EnvironmentPlugin({ 169 | GRAPHQL_CONFIG_ENDPOINT_NAME: '', 170 | NODE_ENV: env 171 | }) 172 | ) 173 | 174 | if (result.parsed) { 175 | config.plugins.push(new Dotenv()) 176 | } 177 | 178 | config.plugins.push(new CleanWebpackPlugin([path.resolve(cwd, opts.outputPath)], {root: cwd})) 179 | 180 | config.plugins.push( 181 | new ExtractTextPlugin({ 182 | filename: '[name].[chunkhash].css' 183 | }) 184 | ) 185 | 186 | if (opts.staticRoot && fs.existsSync(opts.staticRoot)) { 187 | if (config.devServer) config.devServer.contentBase = opts.staticRoot 188 | config.plugins.push(new CopyWebpackPlugin([{from: opts.staticRoot}])) 189 | } 190 | 191 | if (opts.entries.length > 1) { 192 | config.optimization.runtimeChunk = { 193 | name: opts.commonChunkName 194 | } 195 | } 196 | 197 | const {config: graphqlConfig} = tryGetGraphQLProjectConfig() 198 | if (graphqlConfig && graphqlConfig.schemaPath) { 199 | const options = { 200 | schema: path.resolve(cwd, graphqlConfig.schemaPath), 201 | src: path.resolve(cwd, opts.srcRoot), 202 | languagePlugin: null 203 | } 204 | if (fs.existsSync('tsconfig.json')) { 205 | options.languagePlugin = RelayCompilerLanguageTypescript 206 | } 207 | config.plugins = config.plugins.concat([new RelayCompilerWebpackPlugin(options)]) 208 | } 209 | 210 | const directives = Object.assign( 211 | { 212 | defaultSrc: ["'none'"], 213 | baseUri: ["'self'"], 214 | blockAllMixedContent: true, 215 | connectSrc: ["'self'"], 216 | imgSrc: ["'self'"], 217 | scriptSrc: ["'self'"], 218 | styleSrc: ["'self'", "'unsafe-inline'"], 219 | fontSrc: ["'self'"] 220 | }, 221 | opts.cspDirectives || {} 222 | ) 223 | 224 | if (opts.allowGitHubSubresources) { 225 | directives.imgSrc.push('*.githubusercontent.com') 226 | } 227 | 228 | if (isDevServer) { 229 | directives.connectSrc.push('ws:') 230 | } 231 | 232 | const minify = 233 | env === 'production' 234 | ? { 235 | collapseWhitespace: true, 236 | removeComments: true 237 | } 238 | : false 239 | 240 | config.plugins = config.plugins.concat( 241 | Object.keys(config.entry).map( 242 | entry => 243 | new HtmlWebpackPlugin({ 244 | filename: `${entry}.html`, 245 | chunks: [opts.commonChunkName, entry], 246 | template: opts.template, 247 | contentSecurityPolicy: buildContentSecurityPolicy({directives}), 248 | minify 249 | }) 250 | ) 251 | ) 252 | 253 | if (env === 'production') { 254 | config.optimization.minimizer = [new TerserPlugin()] 255 | 256 | // $FlowFixMe 257 | config.plugins = config.plugins.concat([ 258 | new CompressionPlugin({ 259 | test: /\.(js|css)$/ 260 | }) 261 | ]) 262 | } 263 | 264 | const cssLoader = { 265 | loader: 'css-loader', 266 | options: {} 267 | } 268 | const cssLoaders = [cssLoader] 269 | 270 | const postCSSConfig = path.resolve(cwd, 'postcss.config.js') 271 | if (fs.existsSync(postCSSConfig)) { 272 | cssLoader.options.importLoaders = 1 273 | cssLoaders.push({ 274 | loader: 'postcss-loader' 275 | }) 276 | } 277 | 278 | const typescriptConfig = {} 279 | if (fs.existsSync('.babelrc')) { 280 | typescriptConfig.useBabel = true 281 | } 282 | 283 | config.module = { 284 | rules: [ 285 | { 286 | test: /\.tsx?$/, 287 | loader: 'awesome-typescript-loader', 288 | options: typescriptConfig 289 | }, 290 | { 291 | test: /\.js$/, 292 | exclude: /node_modules/, 293 | loader: 'babel-loader' 294 | }, 295 | { 296 | test: /\.css$/, 297 | // exclude: /node_modules/, 298 | use: ExtractTextPlugin.extract({ 299 | fallback: 'style-loader', 300 | use: cssLoaders 301 | }) 302 | } 303 | ] 304 | } 305 | 306 | config.resolve = { 307 | extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx'], 308 | mainFields: ['style', 'browser', 'module', 'main'] 309 | } 310 | 311 | return config 312 | } 313 | 314 | // Get webpack proxy configuration as per .graphqlconfig. 315 | function proxyConfig(path) { 316 | const config = {} 317 | 318 | const {endpointsExtension} = tryGetGraphQLProjectConfig() 319 | if (endpointsExtension) { 320 | const graphqlEndpoint = endpointsExtension.getEndpoint() 321 | 322 | const {url: target, headers} = graphqlEndpoint 323 | const changeOrigin = true 324 | 325 | const pathRewrite = {} 326 | pathRewrite[`^${path}`] = '' 327 | 328 | config[path] = {changeOrigin, headers, pathRewrite, target} 329 | } 330 | 331 | return config 332 | } 333 | 334 | // TODO: Find a better way to attempt to load GraphQL config without erroring 335 | function tryGetGraphQLProjectConfig() { 336 | try { 337 | return getGraphQLProjectConfig() 338 | } catch (error) { 339 | if (error.name === 'ConfigNotFoundError') { 340 | return {} 341 | } else { 342 | throw error 343 | } 344 | } 345 | } 346 | --------------------------------------------------------------------------------