├── .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 |
--------------------------------------------------------------------------------