forked from shapeshift/web
-
Notifications
You must be signed in to change notification settings - Fork 0
/
react-app-rewired.config.js
286 lines (271 loc) · 11.9 KB
/
react-app-rewired.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
/**
* React App Rewired Config
*/
const stableStringify = require('fast-json-stable-stringify')
const fs = require('fs')
const _ = require('lodash')
const path = require('path')
const ssri = require('ssri')
const webpack = require('webpack')
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity')
const headers = require('./headers')
process.env.REACT_APP_CSP_META = headers.cspMeta ?? ''
// The HTML template can pull in static assets from outside of the Webpack
// pipeline; these need SRI too. This generates SRI attributes for each static
// asset, exporting them as predictably-named REACT_APP_SRI_FILENAME_EXT
// environment variables that can be used in the template.
for (const dirent of fs.readdirSync('./public', { withFileTypes: true })) {
if (!dirent.isFile()) continue
const integrity = ssri.fromData(fs.readFileSync(`./public/${dirent.name}`), {
strict: true,
algorithms: ['sha256']
})
const mungedName = dirent.name
.toUpperCase()
.split('')
.map(x => (/^[0-9A-Z]$/.test(x) ? x : '_'))
.join('')
process.env[`REACT_APP_SRI_${mungedName}`] = integrity.toString()
}
module.exports = {
webpack: (config, mode) => {
const isProduction = mode === 'production'
const isDevelopment = mode === 'development'
// Initialize top-level arrays just in case they're missing for some reason.
_.merge(config, {
plugins: [],
ignoreWarnings: []
})
// Webpack 5 no longer bundles polyfills for default Node modules, but we depend on some
// packages that need them to work.
//
// (If https://github.com/facebook/create-react-app/issues/11756 ever gets officially fixed,
// we should probably align with whatever solution is chosen.)
_.merge(config, {
resolve: {
fallback: {
crypto: require.resolve('crypto-browserify'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
stream: require.resolve('stream-browserify'),
zlib: require.resolve('browserify-zlib')
}
},
// Also provide polyfills for some Node globals.
plugins: [
...config.plugins,
new webpack.ProvidePlugin({
Buffer: ['buffer/', 'Buffer'],
process: ['process/browser.js']
})
]
})
// Cloudflare Pages has a max asset size of 25 MiB. Without limiting the chunk size,
// generated chunks or source maps may exceed this limit. 6 MiB seems to keep the max
// gzipped chunk size ~1MiB and the max source map size ~8MiB, which avoids tripping
// CRA's "The bundle size is significantly larger than recommended" warning.
_.merge(
config,
isProduction
? {
optimization: {
splitChunks: {
chunks: 'all',
name: isDevelopment ? undefined : false, // _.merge() ignores undefined
maxSize: 6 * 1024 * 1024
},
// This uses numerically-ascending chunk IDs with no gaps, a la Webpack 4. Webpack 5
// numbers chunks differently, and it's not obvious that they're deterministic. If
// we can determine that chunk ids are deterministic without this option, it can go.
chunkIds: 'natural'
}
}
: undefined
)
// Webpack uses MD4 by default, but SHA-256 can be verified with standard tooling.
_.merge(config, {
output: {
hashFunction: 'sha256'
}
})
// Ignore warnings raised by source-map-loader. Some third party packages ship misconfigured
// sourcemap paths and cause many spurious warnings.
//
// Removable when https://github.com/facebook/create-react-app/pull/11752 is merged upstream.
_.merge(config, {
ignoreWarnings: [
...config.ignoreWarnings,
function ignoreSourceMapLoaderWarnings(warning) {
return (
warning.module?.resource?.includes?.('node_modules') &&
warning.details?.includes?.('source-map-loader')
)
}
]
})
// Remove synthetic CSP/SRI environment variables from DefinePlugin.
_.merge(config, {
plugins: config.plugins.map(plugin => {
if (plugin.constructor.name !== 'DefinePlugin') return plugin
const definitions = JSON.parse(JSON.stringify(plugin.definitions))
const env = definitions['process.env'] || {}
for (const key in env) {
if (/^REACT_APP_(CSP|SRI)_.*$/.test(key)) delete env[key]
}
return new webpack.DefinePlugin(definitions)
})
})
// Generate and embed Subresource Integrity (SRI) attributes for all files.
// Automatically embeds SRI hashes when generating the embedded webpack loaders
// for split code.
_.merge(config, {
output: {
// This is the default, but the SRI spec requires it to be set explicitly.
crossOriginLoading: 'anonymous'
},
// SubresourceIntegrityPlugin automatically disables itself in development.
plugins: [
...config.plugins,
new SubresourceIntegrityPlugin({
hashFuncNames: ['sha256']
})
]
})
// Collect env vars that would have been injected via DefinePlugin. We will emit them
// as dynamically-loaded JSON later instead of find/replacing the string 'process.env'.
// This ensures that the minified Webpack output will be the same no matter the build
// options being used.
const env = Object.fromEntries(
Object.entries(
config.plugins
.filter(plugin => plugin.constructor.name === 'DefinePlugin')
.reduceRight(x => x).definitions?.['process.env'] || {}
)
.filter(([, v]) => v)
.map(([k, v]) => [k, JSON.parse(v)])
)
// Update the Webpack config to emit the collected env vars as `env.json` and load them
// dynamically instead of baking them into each chunk that might reference them.
_.merge(
config,
isProduction
? {
plugins: [
...config.plugins.map(plugin => {
switch (plugin.constructor.name) {
case 'DefinePlugin': {
// Remove all REACT_APP_* 'process.env' entries from DefinePlugin; these will
// be pulled from env.json via src/config.ts and src/env/index.ts.
if (
Object.keys(plugin.definitions).filter(x => x !== 'process.env').length !== 0
) {
throw new Error('Unexpected DefinePlugin entries')
}
const definitions = Object.fromEntries(
Object.entries(env)
.filter(([k]) => !k.startsWith('REACT_APP_'))
.sort((a, b) => {
if (a[0] < b[0]) return -1
if (a[0] > b[0]) return 1
return 0
})
)
console.info('Embedded environment vars:', definitions)
return new webpack.DefinePlugin(
Object.fromEntries(
Object.entries(definitions).map(([k, v]) => [
`process.env.${k}`,
JSON.stringify(v)
])
)
)
}
default:
return plugin
}
})
],
module: {
rules: [
...config.module?.rules,
{
// This rule causes the (placeholder) contents of `src/env/env.json` to be thrown away
// and replaced with `stableStringify(env)`, which is then written out to `build/env.json`.
//
// Note that simply adding this rule doesn't force `env.json` to be generated. That happens
// because `src/env/process.js` `require()`s it, and that module is provided via ProvidePlugin
// above. If nothing ever uses that `process` global, both `src/env/process.js` and `env.json`
// will be omitted from the build.
resource: path.join(__dirname, 'src/env/env.json'),
// Webpack loads resources by reading them from disk and feeding them through a series
// of loaders. It then emits them by feeding the result to a generator.
//
// The `val-loader` plugin is a customizable loader which `require()`s `executableFile`,
// expecting a single exported function which it uses to transform the on-disk module data.
// The stub loader, `src/env/loader.js`, simply takes a `code` function as an option and
// and replaces the module data with its result.
//
// Webpack requires both the module being loaded and the stub loader to exist as actual files
// on the filesystem, but this setup allows the actual content of a module to be generated
// at compile time.
use: [
{
loader: 'val-loader',
options: {
executableFile: require.resolve(path.join(__dirname, 'src/env/loader.js')),
code() {
return stableStringify(
Object.fromEntries(
Object.entries(env).filter(([k]) => k.startsWith('REACT_APP_'))
)
)
}
}
}
],
// The type ['asset/resource'](https://webpack.js.org/guides/asset-modules/#resource-assets)
// tells Webpack to a special generator that will emit the module as a separate file instead
// of inlining its contents into another chunk, as well as skip the normal plugin processing
// and minification steps and just write the raw as-loaded contents. The `generator.filename`
// option overrides the default output path so that the file ends up at `build/env.json`
// instead of the default `build/static/media/env.[hash].json`.
type: 'asset/resource',
generator: {
filename: '[base]'
}
}
]
},
// We can't use `fetch()` to load `env.json` when running tests because Jest doesn't do top-level await.
// We can't manually mock out the fetch because we'd either have to turn on automock, which mocks *everything*
// and breaks a lot of stuff, or call `jest.mock()`, which doesn't exist in the browser. It can't be called
// conditionally because that breaks Jest's magic hoisting BS, and we can't polyfill it because the existence
// of a global `jest` object causes various things to think they're being tested and complain that their
// "test harnesses" aren't set up correctly.
//
// Instead, we leave the jest-friendly behavior as the "default", and use this alias to swap in the behavior
// we want to happen in the browser during the build of the webpack bundle.
resolve: {
alias: {
[path.join(__dirname, 'src/env/index.ts')]: path.join(
__dirname,
'src/env/webpack.ts'
)
}
},
experiments: {
topLevelAwait: true
}
}
: {}
)
return config
},
devServer: configFunction => {
return (proxy, allowedHost) => {
const config = configFunction(proxy, allowedHost)
config.headers = headers.headers
return config
}
}
}